From 878e41dc6959e314235f04fc4b875dd7e6b714a2 Mon Sep 17 00:00:00 2001 From: svenfeld Date: Mon, 2 Sep 2024 22:31:15 +0200 Subject: [PATCH 01/32] Test gh pages --- .../approve_dependabotifications.yml | 27 ----- .github/workflows/deploy_and_release.yml | 104 ----------------- .github/workflows/main_build.yml | 79 +++++++------ .github/workflows/test.yml | 16 --- .github/workflows/version.yml | 107 ------------------ 5 files changed, 45 insertions(+), 288 deletions(-) delete mode 100644 .github/workflows/approve_dependabotifications.yml delete mode 100644 .github/workflows/deploy_and_release.yml delete mode 100644 .github/workflows/test.yml delete mode 100644 .github/workflows/version.yml diff --git a/.github/workflows/approve_dependabotifications.yml b/.github/workflows/approve_dependabotifications.yml deleted file mode 100644 index 947e459a0..000000000 --- a/.github/workflows/approve_dependabotifications.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Dependabot auto-approve -on: - pull_request_target: - paths: ["pom.xml"] - -permissions: - contents: write - pull-requests: write - -jobs: - dependabot: - runs-on: ubuntu-latest - if: github.actor == 'dependabot[bot]' - steps: - - name: Dependabot metadata - id: metadata - uses: dependabot/fetch-metadata@v2 - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - - # Note: If you use status checks to test pull requests, you should enable Require status checks to pass before merging for the target branch for Dependabot pull requests. This branch protection rule ensures that pull requests are not merged unless all the required status checks pass. For more information, see "Managing a branch protection rule." - - name: Enable auto-merge for Dependabot PRs - # if: steps.metadata.outputs.update-type == 'version-update:semver-patch' && contains(steps.metadata.outputs.dependency-names, 'my-dependency') - run: gh pr merge --auto --merge "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/deploy_and_release.yml b/.github/workflows/deploy_and_release.yml deleted file mode 100644 index b2110217e..000000000 --- a/.github/workflows/deploy_and_release.yml +++ /dev/null @@ -1,104 +0,0 @@ -name: Deploy and Release CryptoAnalysis - -on: [workflow_dispatch] - -jobs: - deployment: - runs-on: ubuntu-latest - environment: Deploy - name: CryptoAnalysis Deployment - - steps: - - name: Checkout source code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Java - uses: actions/setup-java@v3 - with: - distribution: 'adopt' - java-package: 'jdk' - java-version: '11' - server-id: 'ossrh' # must match the serverId configured for the nexus-staging-maven-plugin - server-username: OSSRH_USERNAME # Env var that holds your OSSRH user name - server-password: OSSRH_PASSWORD # Env var that holds your OSSRH user pw - gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} # Substituted with the value stored in the referenced secret - gpg-passphrase: SIGN_KEY_PASS # Env var that holds the key's passphrase - - - name: Set up Maven - uses: stCarolas/setup-maven@v4.5 - with: - maven-version: 3.6.3 - - - name: Build & Deploy CryptoAnalysis - run: mvn -B -U clean deploy -Pdeployment -DskipTests - env: - SIGN_KEY_PASS: ${{ secrets.GPG_PRIVATE_KEY_PASSPHRASE }} - OSSRH_USERNAME: ${{ secrets.SONATYPE_USER }} - OSSRH_PASSWORD: ${{ secrets.SONATYPE_PW }} - - - name: Fetch all tags - run: git fetch --tags - - - name: Find built JARs - id: find_jars - run: | - CA_JAR=$(ls CryptoAnalysis/build/*.jar | grep -v 'sources\|javadoc' | head -n 1) - ANDROID_JAR=$(ls CryptoAnalysis-Android/build/*.jar | head -n 1) - echo "CA_JAR=$CA_JAR" >> $GITHUB_ENV - echo "ANDROID_JAR=$ANDROID_JAR" >> $GITHUB_ENV - CA_JAR_BASENAME=$(basename $CA_JAR) - ANDROID_JAR_BASENAME=$(basename $ANDROID_JAR) - echo "CA_JAR_BASENAME=$CA_JAR_BASENAME" >> $GITHUB_ENV - echo "ANDROID_JAR_BASENAME=$ANDROID_JAR_BASENAME" >> $GITHUB_ENV - shell: bash - - - name: Extract Version from pom.xml - id: extract_version - run: | - VERSION=$(sed -n 's/.*\(.*\)<\/version>.*/\1/p' pom.xml | head -n 1) - echo "VERSION=$VERSION" >> $GITHUB_ENV - - - name: Generate Release Notes - id: generate_notes - run: | - LATEST_TAG=$(git tag --sort=-creatordate | head -n 1) - git log $LATEST_TAG..HEAD --merges --pretty=format:"%h" > merged_prs.txt - - RELEASE_NOTES="Release Notes:\n\n" - - while IFS= read -r commit_hash; do - if git log -1 --pretty=format:"%s" $commit_hash | grep -iq "dependabot"; then - continue - fi - - PR_NUMBER=$(git log -1 --pretty=format:"%s" $commit_hash | grep -oE "([Pp][Rr]|pull request) #[0-9]+" | grep -oE "[0-9]+" | head -n 1) - - FIRST_COMMENT=$(gh pr view $PR_NUMBER --json body --jq '.body') - - if [ -n "$FIRST_COMMENT" ]; then - RELEASE_NOTES+="- PR #$PR_NUMBER: $FIRST_COMMENT\n" - fi - done < merged_prs.txt - - echo "RELEASE_NOTES<> $GITHUB_ENV - echo "$RELEASE_NOTES" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - - echo -e "$RELEASE_NOTES" > release_notes.txt - cat release_notes.txt - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Create and Upload Release Assets - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ env.VERSION }} - name: ${{ env.VERSION }} - body_path: release_notes.txt - files: | - ${{ env.CA_JAR }} - ${{ env.ANDROID_JAR }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/main_build.yml b/.github/workflows/main_build.yml index e5adb64a6..4daf12bd9 100644 --- a/.github/workflows/main_build.yml +++ b/.github/workflows/main_build.yml @@ -1,37 +1,48 @@ -name: CryptoAnalysis build - -on: [push, pull_request] +name: Aggregate Results +on: + push: + branches: + - test_gh_pages # Or whichever branch you're working on jobs: - # Builds the project in windows, ubuntu and macos - build: - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - runs-on: ${{ matrix.os }} - name: Project build in ${{ matrix.os }} + build-test-aggregate-deploy: + runs-on: ubuntu-latest + steps: - - name: Checkout source code - uses: actions/checkout@v3 - # Sets up Java version - - name: Set up Java - uses: actions/setup-java@v3 - with: - distribution: 'adopt' - java-package: jdk - java-version: '11' - # Sets up Maven version - - name: Set up Maven - uses: stCarolas/setup-maven@v4.5 - with: - maven-version: 3.6.3 - # Restores Maven dependecies - - name: Restore local Maven repository - uses: actions/cache@v2 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - name: Run maven command - run: mvn clean verify + - name: Checkout code and history + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '11' + + - name: Build and run tests + run: mvn clean install + + - name: Create gh-pages-output directory + run: mkdir -p gh-pages-output + + - name: Aggregate test results + run: | + total_time=$(grep -h "Time elapsed" CryptoAnalysis/shippable/testresults/*.txt | awk '{sum += $3} END {print sum}') + timestamp=$(date +"%Y-%m-%d %H:%M:%S") + echo "${timestamp}, Total Time: ${total_time} seconds" >> gh-pages-output/history.txt + + - name: Generate HTML report + run: | + echo "

Aggregated Test Results

    " > gh-pages-output/index.html + cat gh-pages-output/history.txt | while read line; do + echo "
  • ${line}
  • " >> gh-pages-output/index.html + done + echo "
" >> gh-pages-output/index.html + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./gh-pages-output + keep_files: true # Keep existing files in the gh-pages branch diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index a345dfd22..000000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Test Internal Action - -on: push - -jobs: - internal_action: - runs-on: ubuntu-latest - name: Test CryptoAnalysis Action - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - name: Run CogniCrypt - uses: ./ - with: - appPath: "CryptoAnalysisTargets/HelloWorld/HelloWorld.jar" - basePath: "CryptoAnalysisTargets/HelloWorld" diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml deleted file mode 100644 index 47e52e807..000000000 --- a/.github/workflows/version.yml +++ /dev/null @@ -1,107 +0,0 @@ -name: Version handling - -on: - pull_request: - types: - - closed - branches: - - master - -jobs: - version-update: - # This version does not run on self-opened PRs - if: ${{ github.event.pull_request.merged == true && github.event.pull_request.user.login != 'github-actions[bot]' }} - runs-on: ubuntu-latest - steps: - - name: Checkout source code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - # Sets up Java version - - name: Set up Java - uses: actions/setup-java@v3 - with: - distribution: 'adopt' - java-package: jdk - java-version: '11' - # Sets up Maven version - - name: Set up Maven - uses: stCarolas/setup-maven@v4.5 - with: - maven-version: 3.6.3 - # Semantic versioning - - name: Semantic versioning - id: versioning - uses: paulhatch/semantic-version@v5.1.0 - with: - tag_prefix: "" - # A string which, if present in a git commit, indicates that a change represents a - # major (breaking) change, supports regular expressions wrapped with '/' - major_pattern: "(MAJOR)" - # Same as above except indicating a minor change, supports regular expressions wrapped with '/' - minor_pattern: "(MINOR)" - # A string to determine the format of the version output - version_format: "${major}.${minor}.${patch}" - # Check, whether there is an existing branch "version_" -> "master" - # and store the results as environment variables - - name: Check if branch and PR exist - # The second command was copied from https://stackoverflow.com/questions/73812503/github-action-stop-the-action-if-pr-already-exists - run: | - echo VERSION_BRANCH_EXISTS=$(git ls-remote --heads origin refs/heads/version_${{ steps.versioning.outputs.version }} | wc -l) >> $GITHUB_ENV - echo PR_EXISTS=$(gh pr list \ - --repo "$GITHUB_REPOSITORY" \ - --json baseRefName,headRefName \ - --jq ' - map(select(.baseRefName == "master" and .headRefName == "version_${{ steps.versioning.outputs.version }}")) - | length - ') >> $GITHUB_ENV - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # If the branch "version_" does not exist, create the branch and update the version in all files - - name: Create branch and update CryptoAnalysis version - if: ${{ env.VERSION_BRANCH_EXISTS == '0' }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git checkout -b version_${{ steps.versioning.outputs.version }} - mvn build-helper:parse-version versions:set -DnewVersion=\${{ steps.versioning.outputs.version }} versions:commit - git ls-files | grep 'pom.xml$' | xargs git add - git commit --allow-empty -am "Update CryptoAnalysis version to ${{ steps.versioning.outputs.version }}" - git push origin version_${{ steps.versioning.outputs.version }} - # If a PR "version_" -> "master" does not exist, create the PR - - name: Open pull request for version update - if: ${{ env.PR_EXISTS == '0' }} - run: | - gh pr create -B master -H version_${{ steps.versioning.outputs.version }} -t "Update CryptoAnalysis version to ${{ steps.versioning.outputs.version }}" -b "This PR was created by the version-update workflow. Please make sure to delete the branch after merging, otherwise future workflows might fail." - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - version-release: - # This job runs only on merged PRs, which were opened by the version-update job - if: ${{ github.event.pull_request.merged == true && github.event.pull_request.user.login == 'github-actions[bot]' }} - runs-on: ubuntu-latest - steps: - - name: Checkout source code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - # Semantic versioning - - name: Semantic versioning - id: versioning - uses: paulhatch/semantic-version@v5.1.0 - with: - tag_prefix: "" - # A string which, if present in a git commit, indicates that a change represents a - # major (breaking) change, supports regular expressions wrapped with '/' - major_pattern: "(MAJOR)" - # Same as above except indicating a minor change, supports regular expressions wrapped with '/' - minor_pattern: "(MINOR)" - # A string to determine the format of the version output - version_format: "${major}.${minor}.${patch}" - # Create a tag with the newest version to prepare a release - - name: Create tag for new version - run: | - git config --global user.email "${{ github.actor }}@users.noreply.github.com" - git config --global user.name "${{ github.actor }}" - git tag -a ${{ steps.versioning.outputs.version }} -m "CryptoAnalysis version ${{ steps.versioning.outputs.version }}" - git push origin ${{ steps.versioning.outputs.version }} \ No newline at end of file From b7b0f767eb8196530b16d0e6e7dac4f6de5cb7aa Mon Sep 17 00:00:00 2001 From: svenfeld Date: Mon, 2 Sep 2024 22:59:34 +0200 Subject: [PATCH 02/32] Update main_build.yml Create analysis run time summary page --- .github/workflows/main_build.yml | 146 ++++++++++++++++++++++--------- README.md | 1 + 2 files changed, 107 insertions(+), 40 deletions(-) diff --git a/.github/workflows/main_build.yml b/.github/workflows/main_build.yml index 4daf12bd9..8704e8ec7 100644 --- a/.github/workflows/main_build.yml +++ b/.github/workflows/main_build.yml @@ -2,47 +2,113 @@ name: Aggregate Results on: push: branches: - - test_gh_pages # Or whichever branch you're working on + - test_gh_pages jobs: - build-test-aggregate-deploy: - runs-on: ubuntu-latest - + # Builds the project in windows, ubuntu, and macos + build: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + name: Project build in ${{ matrix.os }} steps: - - name: Checkout code and history - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Set up JDK - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: '11' - - - name: Build and run tests - run: mvn clean install - - - name: Create gh-pages-output directory - run: mkdir -p gh-pages-output - - - name: Aggregate test results - run: | - total_time=$(grep -h "Time elapsed" CryptoAnalysis/shippable/testresults/*.txt | awk '{sum += $3} END {print sum}') - timestamp=$(date +"%Y-%m-%d %H:%M:%S") - echo "${timestamp}, Total Time: ${total_time} seconds" >> gh-pages-output/history.txt - - - name: Generate HTML report - run: | - echo "

Aggregated Test Results

    " > gh-pages-output/index.html - cat gh-pages-output/history.txt | while read line; do - echo "
  • ${line}
  • " >> gh-pages-output/index.html - done - echo "
" >> gh-pages-output/index.html + - name: Checkout source code + uses: actions/checkout@v4 + with: + ref: test_gh_pages + fetch-depth: 0 + # Sets up Java version + - name: Set up Java + uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-package: jdk + java-version: '11' + # Sets up Maven version + - name: Set up Maven + uses: stCarolas/setup-maven@v4.5 + with: + maven-version: 3.6.3 + # Restores Maven dependencies + - name: Restore local Maven repository + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Run maven command + run: mvn clean verify - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./gh-pages-output - keep_files: true # Keep existing files in the gh-pages branch + # Ubuntu-specific steps only + - name: Create or update gh-pages-output directory + if: matrix.os == 'ubuntu-latest' + run: mkdir -p gh-pages-output + + - name: Aggregate test results from multiple directories + if: matrix.os == 'ubuntu-latest' + run: | + total_time=0 + for dir in CryptoAnalysis/shippable/testresults HeadlessJavaScanner/shippable/testresults; do + if [ -d "$dir" ]; then + echo "Processing directory: $dir" + for file in "$dir"/*.txt; do + if [ -f "$file" ]; then + echo "Processing file: $file" + # Extract the time elapsed from each relevant line in the file and sum it up + file_time=$(grep -oP "Time elapsed: \K[\d\.]+" "$file" | awk '{sum += $1} END {print sum}') + if [ -n "$file_time" ]; then + echo "Time found: $file_time seconds" + total_time=$(echo "$total_time + $file_time" | bc) + else + echo "No time elapsed found in file: $file" + fi + else + echo "No files found in directory: $dir" + fi + done + else + echo "Directory does not exist: $dir" + fi + done + + # Convert total time to minutes, seconds, and milliseconds + total_time_int=$(printf "%.0f" "$total_time") + minutes=$((total_time_int / 60)) + seconds=$((total_time_int % 60)) + milliseconds=$(printf "%.0f" "$(echo "($total_time - $total_time_int) * 1000" | bc)") + + echo "Total Time Calculated: ${minutes}m ${seconds}s ${milliseconds}ms" + timestamp=$(date +"%Y-%m-%d %H:%M:%S") + run_number=${{ github.run_number }} + echo "Run ${run_number}, ${timestamp}, Total Time: ${minutes}m ${seconds}s ${milliseconds}ms" >> gh-pages-output/current_history.txt + + - name: Combine current and past history + if: matrix.os == 'ubuntu-latest' + run: | + git checkout gh-pages + if [ -f current_history.txt ]; then + echo "current history" + git checkout test_gh_pages + git checkout gh-pages current_history.txt + cat gh-pages-output/current_history.txt >> current_history.txt + mv current_history.txt gh-pages-output/current_history.txt + fi + + - name: Generate HTML report + if: matrix.os == 'ubuntu-latest' + run: | + echo "

Aggregated Test Results

    " > gh-pages-output/index.html + cat gh-pages-output/current_history.txt | while read line; do + echo "${line}" + echo "
  • ${line}
  • " >> gh-pages-output/index.html + done + echo "
" >> gh-pages-output/index.html + + - name: Deploy to GitHub Pages + if: matrix.os == 'ubuntu-latest' + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./gh-pages-output diff --git a/README.md b/README.md index 9b36e7f25..6ab9da330 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # CogniCryptSAST + This repository contains CogniCryptSAST, the static analysis component for [CogniCrypt](https://www.cognicrypt.org). The static analysis CogniCryptSAST takes rules written in the specification language CrySL as input, and performs a static analysis based on the specification of the rules. CrySL is a domain-specific language (DSL) designed to encode usage specifications for cryptographic From 16046b45bbd0d82979159cfb0b3048e82be3336f Mon Sep 17 00:00:00 2001 From: svenfeld <76941845+svenfeld@users.noreply.github.com> Date: Fri, 27 Sep 2024 09:53:50 +0200 Subject: [PATCH 03/32] Refactor for develop branch --- .github/workflows/main_build.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main_build.yml b/.github/workflows/main_build.yml index 8704e8ec7..275ea0b6d 100644 --- a/.github/workflows/main_build.yml +++ b/.github/workflows/main_build.yml @@ -1,8 +1,6 @@ -name: Aggregate Results -on: - push: - branches: - - test_gh_pages +name: Test and Runtime Results + +on: [push, pull_request] jobs: # Builds the project in windows, ubuntu, and macos @@ -16,7 +14,7 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 with: - ref: test_gh_pages + ref: develop fetch-depth: 0 # Sets up Java version - name: Set up Java From ac4ddac393bb00caf8a81be2668cb4404f5238ff Mon Sep 17 00:00:00 2001 From: svenfeld Date: Fri, 27 Sep 2024 10:00:40 +0200 Subject: [PATCH 04/32] Update for develop Update README.md --- .../approve_dependabotifications.yml | 27 +++++ .github/workflows/test.yml | 16 +++ .github/workflows/version.yml | 107 ++++++++++++++++++ README.md | 1 - 4 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/approve_dependabotifications.yml create mode 100644 .github/workflows/test.yml create mode 100644 .github/workflows/version.yml diff --git a/.github/workflows/approve_dependabotifications.yml b/.github/workflows/approve_dependabotifications.yml new file mode 100644 index 000000000..947e459a0 --- /dev/null +++ b/.github/workflows/approve_dependabotifications.yml @@ -0,0 +1,27 @@ +name: Dependabot auto-approve +on: + pull_request_target: + paths: ["pom.xml"] + +permissions: + contents: write + pull-requests: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: github.actor == 'dependabot[bot]' + steps: + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v2 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + # Note: If you use status checks to test pull requests, you should enable Require status checks to pass before merging for the target branch for Dependabot pull requests. This branch protection rule ensures that pull requests are not merged unless all the required status checks pass. For more information, see "Managing a branch protection rule." + - name: Enable auto-merge for Dependabot PRs + # if: steps.metadata.outputs.update-type == 'version-update:semver-patch' && contains(steps.metadata.outputs.dependency-names, 'my-dependency') + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..a345dfd22 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,16 @@ +name: Test Internal Action + +on: push + +jobs: + internal_action: + runs-on: ubuntu-latest + name: Test CryptoAnalysis Action + steps: + - name: Checkout source code + uses: actions/checkout@v3 + - name: Run CogniCrypt + uses: ./ + with: + appPath: "CryptoAnalysisTargets/HelloWorld/HelloWorld.jar" + basePath: "CryptoAnalysisTargets/HelloWorld" diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml new file mode 100644 index 000000000..47e52e807 --- /dev/null +++ b/.github/workflows/version.yml @@ -0,0 +1,107 @@ +name: Version handling + +on: + pull_request: + types: + - closed + branches: + - master + +jobs: + version-update: + # This version does not run on self-opened PRs + if: ${{ github.event.pull_request.merged == true && github.event.pull_request.user.login != 'github-actions[bot]' }} + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + # Sets up Java version + - name: Set up Java + uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-package: jdk + java-version: '11' + # Sets up Maven version + - name: Set up Maven + uses: stCarolas/setup-maven@v4.5 + with: + maven-version: 3.6.3 + # Semantic versioning + - name: Semantic versioning + id: versioning + uses: paulhatch/semantic-version@v5.1.0 + with: + tag_prefix: "" + # A string which, if present in a git commit, indicates that a change represents a + # major (breaking) change, supports regular expressions wrapped with '/' + major_pattern: "(MAJOR)" + # Same as above except indicating a minor change, supports regular expressions wrapped with '/' + minor_pattern: "(MINOR)" + # A string to determine the format of the version output + version_format: "${major}.${minor}.${patch}" + # Check, whether there is an existing branch "version_" -> "master" + # and store the results as environment variables + - name: Check if branch and PR exist + # The second command was copied from https://stackoverflow.com/questions/73812503/github-action-stop-the-action-if-pr-already-exists + run: | + echo VERSION_BRANCH_EXISTS=$(git ls-remote --heads origin refs/heads/version_${{ steps.versioning.outputs.version }} | wc -l) >> $GITHUB_ENV + echo PR_EXISTS=$(gh pr list \ + --repo "$GITHUB_REPOSITORY" \ + --json baseRefName,headRefName \ + --jq ' + map(select(.baseRefName == "master" and .headRefName == "version_${{ steps.versioning.outputs.version }}")) + | length + ') >> $GITHUB_ENV + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # If the branch "version_" does not exist, create the branch and update the version in all files + - name: Create branch and update CryptoAnalysis version + if: ${{ env.VERSION_BRANCH_EXISTS == '0' }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git checkout -b version_${{ steps.versioning.outputs.version }} + mvn build-helper:parse-version versions:set -DnewVersion=\${{ steps.versioning.outputs.version }} versions:commit + git ls-files | grep 'pom.xml$' | xargs git add + git commit --allow-empty -am "Update CryptoAnalysis version to ${{ steps.versioning.outputs.version }}" + git push origin version_${{ steps.versioning.outputs.version }} + # If a PR "version_" -> "master" does not exist, create the PR + - name: Open pull request for version update + if: ${{ env.PR_EXISTS == '0' }} + run: | + gh pr create -B master -H version_${{ steps.versioning.outputs.version }} -t "Update CryptoAnalysis version to ${{ steps.versioning.outputs.version }}" -b "This PR was created by the version-update workflow. Please make sure to delete the branch after merging, otherwise future workflows might fail." + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + version-release: + # This job runs only on merged PRs, which were opened by the version-update job + if: ${{ github.event.pull_request.merged == true && github.event.pull_request.user.login == 'github-actions[bot]' }} + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + # Semantic versioning + - name: Semantic versioning + id: versioning + uses: paulhatch/semantic-version@v5.1.0 + with: + tag_prefix: "" + # A string which, if present in a git commit, indicates that a change represents a + # major (breaking) change, supports regular expressions wrapped with '/' + major_pattern: "(MAJOR)" + # Same as above except indicating a minor change, supports regular expressions wrapped with '/' + minor_pattern: "(MINOR)" + # A string to determine the format of the version output + version_format: "${major}.${minor}.${patch}" + # Create a tag with the newest version to prepare a release + - name: Create tag for new version + run: | + git config --global user.email "${{ github.actor }}@users.noreply.github.com" + git config --global user.name "${{ github.actor }}" + git tag -a ${{ steps.versioning.outputs.version }} -m "CryptoAnalysis version ${{ steps.versioning.outputs.version }}" + git push origin ${{ steps.versioning.outputs.version }} \ No newline at end of file diff --git a/README.md b/README.md index 6ab9da330..9b36e7f25 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # CogniCryptSAST - This repository contains CogniCryptSAST, the static analysis component for [CogniCrypt](https://www.cognicrypt.org). The static analysis CogniCryptSAST takes rules written in the specification language CrySL as input, and performs a static analysis based on the specification of the rules. CrySL is a domain-specific language (DSL) designed to encode usage specifications for cryptographic From 98f8d674bc33d3e80c56bc12fc253fd82285bf8e Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Tue, 10 Dec 2024 14:51:50 +0100 Subject: [PATCH 05/32] Create own action for performance analysis --- .github/workflows/performance.yml | 112 ++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 .github/workflows/performance.yml diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml new file mode 100644 index 000000000..712b1a792 --- /dev/null +++ b/.github/workflows/performance.yml @@ -0,0 +1,112 @@ +name: Test and Runtime Results + +on: [workflow_dispatch] + +jobs: + # Builds the project in windows, ubuntu, and macos + build: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + name: Project build in ${{ matrix.os }} + steps: + - name: Checkout source code + uses: actions/checkout@v4 + with: + ref: develop + fetch-depth: 0 + # Sets up Java version + - name: Set up Java + uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-package: jdk + java-version: '11' + # Sets up Maven version + - name: Set up Maven + uses: stCarolas/setup-maven@v4.5 + with: + maven-version: 3.6.3 + # Restores Maven dependencies + - name: Restore local Maven repository + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Run maven command + run: mvn clean verify + + # Ubuntu-specific steps only + - name: Create or update gh-pages-output directory + if: matrix.os == 'ubuntu-latest' + run: mkdir -p gh-pages-output + + - name: Aggregate test results from multiple directories + if: matrix.os == 'ubuntu-latest' + run: | + total_time=0 + for dir in CryptoAnalysis/shippable/testresults HeadlessJavaScanner/shippable/testresults; do + if [ -d "$dir" ]; then + echo "Processing directory: $dir" + for file in "$dir"/*.txt; do + if [ -f "$file" ]; then + echo "Processing file: $file" + # Extract the time elapsed from each relevant line in the file and sum it up + file_time=$(grep -oP "Time elapsed: \K[\d\.]+" "$file" | awk '{sum += $1} END {print sum}') + if [ -n "$file_time" ]; then + echo "Time found: $file_time seconds" + total_time=$(echo "$total_time + $file_time" | bc) + else + echo "No time elapsed found in file: $file" + fi + else + echo "No files found in directory: $dir" + fi + done + else + echo "Directory does not exist: $dir" + fi + done + + # Convert total time to minutes, seconds, and milliseconds + total_time_int=$(printf "%.0f" "$total_time") + minutes=$((total_time_int / 60)) + seconds=$((total_time_int % 60)) + milliseconds=$(printf "%.0f" "$(echo "($total_time - $total_time_int) * 1000" | bc)") + + echo "Total Time Calculated: ${minutes}m ${seconds}s ${milliseconds}ms" + timestamp=$(date +"%Y-%m-%d %H:%M:%S") + run_number=${{ github.run_number }} + echo "Run ${run_number}, ${timestamp}, Total Time: ${minutes}m ${seconds}s ${milliseconds}ms" >> gh-pages-output/current_history.txt + + - name: Combine current and past history + if: matrix.os == 'ubuntu-latest' + run: | + git checkout gh-pages + if [ -f current_history.txt ]; then + echo "current history" + git checkout test_gh_pages + git checkout gh-pages current_history.txt + cat gh-pages-output/current_history.txt >> current_history.txt + mv current_history.txt gh-pages-output/current_history.txt + fi + + - name: Generate HTML report + if: matrix.os == 'ubuntu-latest' + run: | + echo "

Aggregated Test Results

    " > gh-pages-output/index.html + cat gh-pages-output/current_history.txt | while read line; do + echo "${line}" + echo "
  • ${line}
  • " >> gh-pages-output/index.html + done + echo "
" >> gh-pages-output/index.html + + - name: Deploy to GitHub Pages + if: matrix.os == 'ubuntu-latest' + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./gh-pages-output From 1d40e8a2e5e17f7d2a31c2a3f0fc5832da2468aa Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Tue, 10 Dec 2024 15:01:50 +0100 Subject: [PATCH 06/32] Adapt performance analysis action --- .github/workflows/performance.yml | 38 +++++++++++-------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 712b1a792..afac869c7 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -1,33 +1,26 @@ -name: Test and Runtime Results +name: Performance Analysis on: [workflow_dispatch] +env: + JAVA_VERSION: 17 + jobs: - # Builds the project in windows, ubuntu, and macos - build: - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - runs-on: ${{ matrix.os }} - name: Project build in ${{ matrix.os }} + performance: + runs-on: ubuntu-latest steps: - name: Checkout source code uses: actions/checkout@v4 with: - ref: develop + ref: gh-pages fetch-depth: 0 # Sets up Java version - - name: Set up Java - uses: actions/setup-java@v3 + - name: Set up Java ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 with: - distribution: 'adopt' + distribution: adopt java-package: jdk - java-version: '11' - # Sets up Maven version - - name: Set up Maven - uses: stCarolas/setup-maven@v4.5 - with: - maven-version: 3.6.3 + java-version: ${{ env.JAVA_VERSION }} # Restores Maven dependencies - name: Restore local Maven repository uses: actions/cache@v2 @@ -37,18 +30,16 @@ jobs: restore-keys: | ${{ runner.os }}-maven- - name: Run maven command - run: mvn clean verify + run: mvn clean verify -DrunAllTests # Ubuntu-specific steps only - name: Create or update gh-pages-output directory - if: matrix.os == 'ubuntu-latest' run: mkdir -p gh-pages-output - name: Aggregate test results from multiple directories - if: matrix.os == 'ubuntu-latest' run: | total_time=0 - for dir in CryptoAnalysis/shippable/testresults HeadlessJavaScanner/shippable/testresults; do + for dir in CryptoAnalysis/shippable/testresults HeadlessAndroidScanner/shippable/testresults HeadlessJavaScanner/shippable/testresults; do if [ -d "$dir" ]; then echo "Processing directory: $dir" for file in "$dir"/*.txt; do @@ -83,7 +74,6 @@ jobs: echo "Run ${run_number}, ${timestamp}, Total Time: ${minutes}m ${seconds}s ${milliseconds}ms" >> gh-pages-output/current_history.txt - name: Combine current and past history - if: matrix.os == 'ubuntu-latest' run: | git checkout gh-pages if [ -f current_history.txt ]; then @@ -95,7 +85,6 @@ jobs: fi - name: Generate HTML report - if: matrix.os == 'ubuntu-latest' run: | echo "

Aggregated Test Results

    " > gh-pages-output/index.html cat gh-pages-output/current_history.txt | while read line; do @@ -105,7 +94,6 @@ jobs: echo "
" >> gh-pages-output/index.html - name: Deploy to GitHub Pages - if: matrix.os == 'ubuntu-latest' uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} From eeb702ef787105874810d17cf32acbdd81cfca4b Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Tue, 10 Dec 2024 15:05:16 +0100 Subject: [PATCH 07/32] Bring back test reports --- pom.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index ab94f6bff..f91e4dee8 100644 --- a/pom.xml +++ b/pom.xml @@ -287,6 +287,7 @@ -Xmx8G -Xms256M -Xss8M -Dmaven.home="${maven.home}" false + ${project.basedir}/shippable/testresults @@ -450,8 +451,8 @@ + contexts because they require specific keys from the GitHub remote. Include + this profile by setting the -Pdeployment flag. --> deployment From 29819c48cdbe8b18b9c0ac5fffa927bc53650b5e Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Tue, 10 Dec 2024 15:14:57 +0100 Subject: [PATCH 08/32] Set to push to activate it once --- .github/workflows/performance.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index afac869c7..7fe605cc8 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -1,6 +1,6 @@ name: Performance Analysis -on: [workflow_dispatch] +on: [push] env: JAVA_VERSION: 17 From 423c7a80da97b4f0d90b89c8a8f2d56cd7b68336 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Tue, 10 Dec 2024 15:34:28 +0100 Subject: [PATCH 09/32] Update performance.yml --- .github/workflows/performance.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 7fe605cc8..b6d1e5a74 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -12,7 +12,6 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 with: - ref: gh-pages fetch-depth: 0 # Sets up Java version - name: Set up Java ${{ env.JAVA_VERSION }} @@ -23,7 +22,7 @@ jobs: java-version: ${{ env.JAVA_VERSION }} # Restores Maven dependencies - name: Restore local Maven repository - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} From 9947c8cc32c7585baa6179c063fa194ea3fbfbfd Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Tue, 10 Dec 2024 17:19:10 +0100 Subject: [PATCH 10/32] Update performance.yml --- .github/workflows/performance.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index b6d1e5a74..18bd18764 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -29,7 +29,7 @@ jobs: restore-keys: | ${{ runner.os }}-maven- - name: Run maven command - run: mvn clean verify -DrunAllTests + run: mvn clean verify -DrunAnalysisTests # Ubuntu-specific steps only - name: Create or update gh-pages-output directory @@ -38,7 +38,7 @@ jobs: - name: Aggregate test results from multiple directories run: | total_time=0 - for dir in CryptoAnalysis/shippable/testresults HeadlessAndroidScanner/shippable/testresults HeadlessJavaScanner/shippable/testresults; do + for dir in CryptoAnalysis/shippable/testresults; do if [ -d "$dir" ]; then echo "Processing directory: $dir" for file in "$dir"/*.txt; do From 5992e21558b45278bdeca296b903a6adb1b9c927 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Tue, 10 Dec 2024 18:16:39 +0100 Subject: [PATCH 11/32] Trigger performance analysis manually --- .github/workflows/performance.yml | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 18bd18764..a38eb5c39 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -1,12 +1,27 @@ -name: Performance Analysis +name: Performance Analysis (Pre) -on: [push] +on: + push: + workflow_dispatch: + inputs: + analysis: + type: boolean + required: false + default: false + android: + type: boolean + required: false + default: false + soot: + type: boolean + required: false + default: false env: JAVA_VERSION: 17 jobs: - performance: + performance-pre: runs-on: ubuntu-latest steps: - name: Checkout source code @@ -77,7 +92,6 @@ jobs: git checkout gh-pages if [ -f current_history.txt ]; then echo "current history" - git checkout test_gh_pages git checkout gh-pages current_history.txt cat gh-pages-output/current_history.txt >> current_history.txt mv current_history.txt gh-pages-output/current_history.txt From 7ac8c529934c91befe9fb13f4847afb88af833b1 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Thu, 12 Dec 2024 18:24:16 +0100 Subject: [PATCH 12/32] Rework performance action --- .github/actions/performance/action.yml | 70 +++++++ .github/workflows/performance.yml | 242 ++++++++++++++++--------- 2 files changed, 226 insertions(+), 86 deletions(-) create mode 100644 .github/actions/performance/action.yml diff --git a/.github/actions/performance/action.yml b/.github/actions/performance/action.yml new file mode 100644 index 000000000..e9a8d4d09 --- /dev/null +++ b/.github/actions/performance/action.yml @@ -0,0 +1,70 @@ +name: Test Performance + +inputs: + test-results-path: + description: "Path to the test results directory" + required: true + source-branch: + description: "Branch that holds the file with previous results" + required: true + current-results: + description: "File that stores previous results" + required: true + output-file: + description: "File where the results are storted in" + required: true + +runs: + using: "composite" + steps: + - name: Aggregate test results from multiple directories + run: | + total_time=0 + + if [ -d ${{ inputs.test-results-path }} ]; then + echo "Processing directory: ${{ inputs.test-results-path }}" + + for file in ${{ inputs.test-results-path }}/*.txt; do + if [ -f "$file" ]; then + echo "Processing file: $file" + + # Extract the time elapsed from each relevant line in the file and sum it up + file_time=$(grep -oP "Time elapsed: \K[\d\.]+" "$file" | awk '{sum += $1} END {print sum}') + if [ -n "$file_time" ]; then + echo "Time found: $file_time seconds" + total_time=$(echo "$total_time + $file_time" | bc) + else + echo "No time elapsed found in file: $file" + fi + else + echo "No files found in directory: ${{ inputs.test-results-path }}" + fi + done + else + echo "Directory does not exist: ${{ inputs.test-results-path }}" + fi + + # Convert total time to minutes, seconds, and milliseconds + total_time_int=$(printf "%.0f" "$total_time") + minutes=$((total_time_int / 60)) + seconds=$((total_time_int % 60)) + milliseconds=$(printf "%.0f" "$(echo "($total_time - $total_time_int) * 1000" | bc)") + + echo "Total Time Calculated: ${minutes}m ${seconds}s ${milliseconds}ms" + timestamp=$(date +"%Y-%m-%d %H:%M:%S") + echo "${timestamp}, Total Time: ${minutes}m ${seconds}s ${milliseconds}ms" >> ${{ inputs.output-file }} + shell: bash + + - name: Combine current and past results + run: | + git checkout ${{ inputs.source-branch }} + if [ -f ${{ inputs.current-results }} ]; then + git checkout ${{ inputs.source-branch }} ${{ inputs.current-results }} + cat ${{ inputs.output-file }} >> ${{ inputs.current-results }} + mv ${{ inputs.current-results }} ${{ inputs.output-file }} + fi + shell: bash + + - name: Switch to default branch + run: git checkout ${{ github.ref_name }} + shell: bash diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index a38eb5c39..c1a712214 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -1,113 +1,183 @@ -name: Performance Analysis (Pre) +name: Performance Analysis on: push: workflow_dispatch: - inputs: - analysis: - type: boolean - required: false - default: false - android: - type: boolean - required: false - default: false - soot: - type: boolean - required: false - default: false + #inputs: + # analysis: + # type: boolean + # required: false + # default: false + # android: + # type: boolean + # required: false + # default: false + # soot: + # type: boolean + # required: false + # default: false env: JAVA_VERSION: 17 jobs: - performance-pre: + performance-analysis: runs-on: ubuntu-latest steps: - name: Checkout source code uses: actions/checkout@v4 with: fetch-depth: 0 - # Sets up Java version - - name: Set up Java ${{ env.JAVA_VERSION }} - uses: actions/setup-java@v4 + + - name: Build and test Analysis + uses: ./.github/actions/analysis with: - distribution: adopt - java-package: jdk java-version: ${{ env.JAVA_VERSION }} - # Restores Maven dependencies - - name: Restore local Maven repository - uses: actions/cache@v4 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - name: Run maven command - run: mvn clean verify -DrunAnalysisTests - - # Ubuntu-specific steps only + - name: Create or update gh-pages-output directory run: mkdir -p gh-pages-output - - - name: Aggregate test results from multiple directories - run: | - total_time=0 - for dir in CryptoAnalysis/shippable/testresults; do - if [ -d "$dir" ]; then - echo "Processing directory: $dir" - for file in "$dir"/*.txt; do - if [ -f "$file" ]; then - echo "Processing file: $file" - # Extract the time elapsed from each relevant line in the file and sum it up - file_time=$(grep -oP "Time elapsed: \K[\d\.]+" "$file" | awk '{sum += $1} END {print sum}') - if [ -n "$file_time" ]; then - echo "Time found: $file_time seconds" - total_time=$(echo "$total_time + $file_time" | bc) - else - echo "No time elapsed found in file: $file" - fi - else - echo "No files found in directory: $dir" - fi - done - else - echo "Directory does not exist: $dir" - fi - done - - # Convert total time to minutes, seconds, and milliseconds - total_time_int=$(printf "%.0f" "$total_time") - minutes=$((total_time_int / 60)) - seconds=$((total_time_int % 60)) - milliseconds=$(printf "%.0f" "$(echo "($total_time - $total_time_int) * 1000" | bc)") - - echo "Total Time Calculated: ${minutes}m ${seconds}s ${milliseconds}ms" - timestamp=$(date +"%Y-%m-%d %H:%M:%S") - run_number=${{ github.run_number }} - echo "Run ${run_number}, ${timestamp}, Total Time: ${minutes}m ${seconds}s ${milliseconds}ms" >> gh-pages-output/current_history.txt - - - name: Combine current and past history - run: | - git checkout gh-pages - if [ -f current_history.txt ]; then - echo "current history" - git checkout gh-pages current_history.txt - cat gh-pages-output/current_history.txt >> current_history.txt - mv current_history.txt gh-pages-output/current_history.txt - fi - + + - name: Extract performance + uses: ./.github/actions/performance + with: + test-results-path: CryptoAnalysis/shippable/testresults + source-branch: gh-pages + current-results: analysis_history.txt + output-file: gh-pages-output/analysis_history.txt + + - name: Store Analysis performance + uses: actions/upload-artifact@v4 + with: + name: analysis-performance + path: gh-pages-output/analysis_history.txt + + performance-android: + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build and test Android Scanner + uses: ./.github/actions/scanner-android + with: + java-version: ${{ env.JAVA_VERSION }} + + - name: Create or update gh-pages-output directory + run: mkdir -p gh-pages-output + + - name: Extract performance + uses: ./.github/actions/performance + with: + test-results-path: HeadlessAndroidScanner/shippable/testresults + source-branch: gh-pages + current-results: android_history.txt + output-file: gh-pages-output/android_history.txt + + - name: Store Android performance + uses: actions/upload-artifact@v4 + with: + name: android-performance + path: gh-pages-output/android_history.txt + + performance-soot: + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Build and test JavaScanner with Soot + uses: ./.github/actions/scanner-soot + with: + java-version: ${{ env.JAVA_VERSION }} + + - name: Create or update gh-pages-output directory + run: mkdir -p gh-pages-output + + - name: Extract performance + uses: ./.github/actions/performance + with: + test-results-path: HeadlessJavaScanner/shippable/testresults + source-branch: gh-pages + current-results: soot_history.txt + output-file: gh-pages-output/soot_history.txt + + - name: Store Soot performance + uses: actions/upload-artifact@v4 + with: + name: soot-performance + path: gh-pages-output/soot_history.txt + + performance-report: + runs-on: ubuntu-latest + needs: [performance-analysis, performance-android, performance-soot] + steps: + - name: Checkout source code + uses: actions/checkout@v4 + with: + ref: gh-pages + + - name: Load performance files + uses: actions/download-artifact@v4 + with: + path: gh-pages-output + merge-multiple: true + - name: Generate HTML report run: | - echo "

Aggregated Test Results

    " > gh-pages-output/index.html - cat gh-pages-output/current_history.txt | while read line; do - echo "${line}" - echo "
  • ${line}
  • " >> gh-pages-output/index.html - done - echo "
" >> gh-pages-output/index.html + echo "" > gh-pages-output/index.html + echo "
" >> gh-pages-output/index.html + + echo "

Analysis

    " >> gh-pages-output/index.html + + if [ -f gh-pages-output/analysis_history.txt ]; then + cat gh-pages-output/analysis_history.txt | while read line; do + echo "${line}" + echo "
  • ${line}
  • " >> gh-pages-output/index.html + done + fi + + echo "
" >> gh-pages-output/index.html + + echo "

AndroidScanner

    " >> gh-pages-output/index.html + + if [ -f gh-pages-output/android_history.txt ]; then + cat gh-pages-output/android_history.txt | while read line; do + echo "${line}" + echo "
  • ${line}
  • " >> gh-pages-output/index.html + done + fi + + echo "
" >> gh-pages-output/index.html + + echo "

JavaScanner (Soot)

    " >> gh-pages-output/index.html + + if [ -f gh-pages-output/soot_history.txt ]; then + cat gh-pages-output/soot_history.txt | while read line; do + echo "${line}" + echo "
  • ${line}
  • " >> gh-pages-output/index.html + done + fi + + echo "
" >> gh-pages-output/index.html + + echo "
" >> gh-pages-output/index.html + echo "" >> gh-pages-output/index.html - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./gh-pages-output + + - name: Delete artifacts + uses: geekyeggo/delete-artifact@v5 + with: + failOnError: false + name: | + analysis-performance + android-performance + soot-performance From 1fcf9133d234db016e764fa7b265610c83f19563 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Thu, 12 Dec 2024 18:25:14 +0100 Subject: [PATCH 13/32] Remove push trigger --- .github/workflows/build_and_test.yml | 5 ----- .github/workflows/performance.yml | 1 - 2 files changed, 6 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 4efc3ade8..9d39a28fe 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -2,12 +2,7 @@ name: Build And Test (Basic) description: Basic build and tests for the submodules on each push and pull request on: - push: - branches-ignore: - - master - - develop pull_request: - types: [opened, reopened] env: JAVA_VERSION: 17 diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index c1a712214..3d6f63026 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -1,7 +1,6 @@ name: Performance Analysis on: - push: workflow_dispatch: #inputs: # analysis: From 0f7fa9a1c091dfeee5363444ee21b5c11ce4f858 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Sat, 14 Dec 2024 10:56:24 +0100 Subject: [PATCH 14/32] Adapt to Java 17 constructs --- .../analysis/AlternativeReqPredicate.java | 26 ++---- .../AnalysisSeedWithEnsuredPredicate.java | 11 +-- .../AnalysisSeedWithSpecification.java | 91 ++++++------------- .../analysis/EnsuredCrySLPredicate.java | 15 +-- .../java/crypto/analysis/IAnalysisSeed.java | 85 ++--------------- .../analysis/RequiredCrySLPredicate.java | 27 ++---- .../errors/AbstractConstraintsError.java | 23 +++++ .../crypto/analysis/errors/AbstractError.java | 32 ++----- .../analysis/errors/AbstractOrderError.java | 23 +++++ .../errors/AbstractRequiresError.java | 23 +++++ .../crypto/analysis/errors/CallToError.java | 20 ++-- .../analysis/errors/ConstraintError.java | 75 +++++---------- .../analysis/errors/ForbiddenMethodError.java | 35 ++----- .../analysis/errors/HardCodedError.java | 30 ++---- .../errors/ImpreciseValueExtractionError.java | 21 ++--- .../errors/IncompleteOperationError.java | 22 ++--- .../analysis/errors/InstanceOfError.java | 30 ++---- .../analysis/errors/NeverTypeOfError.java | 30 ++---- .../crypto/analysis/errors/NoCallToError.java | 7 +- .../errors/PredicateContradictionError.java | 21 ++--- .../errors/RequiredPredicateError.java | 2 +- .../analysis/errors/TypestateError.java | 21 ++--- .../errors/UncaughtExceptionError.java | 19 +--- .../constraints/ComparisonConstraint.java | 4 +- .../crypto/constraints/ConstraintSolver.java | 18 ++-- .../constraints/EvaluableConstraint.java | 28 +++--- .../constraints/PredicateConstraint.java | 30 +++--- .../CallSiteWithExtractedValue.java | 54 +---------- .../CallSiteWithParamIndex.java | 58 +----------- .../ExtractParameterAnalysis.java | 7 +- .../ExtractParameterQuery.java | 12 +-- .../extractparameter/ExtractedValue.java | 60 +----------- .../crypto/reporting/ReportGenerator.java | 1 - .../typestate/LabeledMatcherTransition.java | 15 ++- .../typestate/ReportingErrorStateNode.java | 22 +---- .../java/crypto/typestate/WrappedState.java | 15 +-- .../assertions/ExtractedValueAssertion.java | 6 +- 37 files changed, 295 insertions(+), 724 deletions(-) create mode 100644 CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractConstraintsError.java create mode 100644 CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractOrderError.java create mode 100644 CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiresError.java diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/AlternativeReqPredicate.java b/CryptoAnalysis/src/main/java/crypto/analysis/AlternativeReqPredicate.java index ec51d68ef..a15a9c7a6 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/AlternativeReqPredicate.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/AlternativeReqPredicate.java @@ -5,6 +5,7 @@ import crysl.rule.ISLConstraint; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; public class AlternativeReqPredicate implements ISLConstraint { @@ -22,30 +23,15 @@ public AlternativeReqPredicate(CrySLPredicate alternativeOne, Statement stmt, in @Override public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((alternatives == null) ? 0 : alternatives.hashCode()); - result = prime * result + ((stmt == null) ? 0 : stmt.hashCode()); - result = prime * result + paramIndex; - return result; + return Objects.hash(alternatives, stmt, paramIndex); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (getClass() != obj.getClass()) return false; - AlternativeReqPredicate other = (AlternativeReqPredicate) obj; - if (alternatives == null) { - if (other.alternatives != null) return false; - } else if (!alternatives.equals(other.alternatives)) return false; - if (stmt == null) { - return other.stmt == null; - } else if (!stmt.equals(other.stmt)) { - return false; - } - - return paramIndex == other.paramIndex; + return obj instanceof AlternativeReqPredicate other + && Objects.equals(alternatives, other.alternatives) + && Objects.equals(stmt, other.stmt) + && paramIndex == other.paramIndex; } public Statement getLocation() { diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithEnsuredPredicate.java b/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithEnsuredPredicate.java index eee9d5c3b..21ae0df22 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithEnsuredPredicate.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithEnsuredPredicate.java @@ -9,6 +9,7 @@ import com.google.common.collect.Multimap; import crysl.rule.CrySLPredicate; import java.util.Collection; +import java.util.Objects; import typestate.TransitionFunction; public class AnalysisSeedWithEnsuredPredicate extends IAnalysisSeed { @@ -110,17 +111,15 @@ public void addEnsuredPredicate(EnsuredCrySLPredicate predicate) { expectedPredicates.get(statement); for (ExpectedPredicateOnSeed predOnSeed : predicateOnSeeds) { - if (!predOnSeed.getPredicate().equals(predicate.getPredicate())) { + if (!predOnSeed.predicate().equals(predicate.getPredicate())) { continue; } - if (!(predOnSeed.getSeed() instanceof AnalysisSeedWithSpecification)) { + if (!(predOnSeed.seed() instanceof AnalysisSeedWithSpecification seedWithSpec)) { continue; } - AnalysisSeedWithSpecification seedWithSpec = - (AnalysisSeedWithSpecification) predOnSeed.getSeed(); - seedWithSpec.addEnsuredPredicate(predicate, statement, predOnSeed.getParamIndex()); + seedWithSpec.addEnsuredPredicate(predicate, statement, predOnSeed.paramIndex()); } } @@ -131,7 +130,7 @@ public void addEnsuredPredicate(EnsuredCrySLPredicate predicate) { @Override public int hashCode() { - return super.hashCode(); + return Objects.hash(super.hashCode()); } @Override diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java b/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java index bea8f9bd2..56d4ae1d6 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java @@ -40,6 +40,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import typestate.TransitionFunction; @@ -172,12 +173,10 @@ private void evaluateTypestateOrder() { private void typeStateChangeAtStatement(Statement statement, State stateNode) { if (typeStateChange.put(statement, stateNode)) { - if (stateNode instanceof ReportingErrorStateNode) { - ReportingErrorStateNode errorStateNode = (ReportingErrorStateNode) stateNode; - + if (stateNode instanceof ReportingErrorStateNode errorStateNode) { TypestateError typestateError = new TypestateError( - this, statement, specification, errorStateNode.getExpectedCalls()); + this, statement, specification, errorStateNode.expectedCalls()); this.addError(typestateError); scanner.getAnalysisReporter().reportError(this, typestateError); } @@ -202,11 +201,10 @@ private void evaluateIncompleteOperations() { continue; } - if (!(n.to() instanceof WrappedState)) { + if (!(n.to() instanceof WrappedState wrappedState)) { continue; } - WrappedState wrappedState = (WrappedState) n.to(); for (TransitionEdge t : specification.getUsagePattern().getAllTransitions()) { if (t.getLeft().equals(wrappedState.delegate()) && !t.from().equals(t.to())) { Collection labels = t.getLabel(); @@ -322,15 +320,11 @@ public void computeExpectedPredicates(Collection seeds) { private void initializeDependantSeedsFromRequiredPredicates(Collection seeds) { Multimap> reqPreds = HashMultimap.create(); for (ISLConstraint constraint : constraintSolver.getRequiredPredicates()) { - if (constraint instanceof RequiredCrySLPredicate) { - RequiredCrySLPredicate reqPred = (RequiredCrySLPredicate) constraint; - + if (constraint instanceof RequiredCrySLPredicate reqPred) { Map.Entry entry = new AbstractMap.SimpleEntry<>(reqPred.getPred(), reqPred.getParamIndex()); reqPreds.put(reqPred.getLocation(), entry); - } else if (constraint instanceof AlternativeReqPredicate) { - AlternativeReqPredicate altPred = (AlternativeReqPredicate) constraint; - + } else if (constraint instanceof AlternativeReqPredicate altPred) { for (CrySLPredicate predicate : altPred.getAlternatives()) { Map.Entry entry = new AbstractMap.SimpleEntry<>(predicate, altPred.getParamIndex()); @@ -644,22 +638,16 @@ private void ensurePredicateAtStatement(EnsuredCrySLPredicate ensPred, Statement Collection expectedPredsAtStatement = expectedPredicates.get(statement); for (ExpectedPredicateOnSeed expectedPredicateOnSeed : expectedPredsAtStatement) { - CrySLPredicate predicate = expectedPredicateOnSeed.getPredicate(); - IAnalysisSeed seed = expectedPredicateOnSeed.getSeed(); - int paramIndex = expectedPredicateOnSeed.getParamIndex(); + CrySLPredicate predicate = expectedPredicateOnSeed.predicate(); + IAnalysisSeed seed = expectedPredicateOnSeed.seed(); + int paramIndex = expectedPredicateOnSeed.paramIndex(); if (predicate.equals(ensPred.getPredicate())) { - if (seed instanceof AnalysisSeedWithSpecification) { - AnalysisSeedWithSpecification seedWithSpec = - (AnalysisSeedWithSpecification) seed; - + if (seed instanceof AnalysisSeedWithSpecification seedWithSpec) { seedWithSpec.addEnsuredPredicateFromOtherRule(ensPred, statement, paramIndex); scanner.getAnalysisReporter() .onGeneratedPredicate(this, ensPred, seedWithSpec, statement); - } else if (seed instanceof AnalysisSeedWithEnsuredPredicate) { - AnalysisSeedWithEnsuredPredicate seedWithoutSpec = - (AnalysisSeedWithEnsuredPredicate) seed; - + } else if (seed instanceof AnalysisSeedWithEnsuredPredicate seedWithoutSpec) { seedWithoutSpec.addEnsuredPredicate(ensPred); scanner.getAnalysisReporter() .onGeneratedPredicate(this, ensPred, seedWithoutSpec, statement); @@ -708,9 +696,7 @@ private Collection getStatesAtStatement(Statement statement) { public void addEnsuredPredicate( EnsuredCrySLPredicate ensPred, Statement statement, int paramIndex) { - if (ensPred instanceof HiddenPredicate) { - HiddenPredicate hiddenPredicate = (HiddenPredicate) ensPred; - + if (ensPred instanceof HiddenPredicate hiddenPredicate) { Map.Entry predAtIndex = new AbstractMap.SimpleEntry<>(hiddenPredicate, paramIndex); hiddenPredicates.put(statement, predAtIndex); @@ -771,12 +757,10 @@ private boolean isPredicateNegatingState(CrySLPredicate ensPred, State stateNode // Negated predicate does not have a condition, i.e. no "after" -> predicate is negated // in all states - if (!(negPred instanceof CrySLCondPredicate)) { + if (!(negPred instanceof CrySLCondPredicate condNegPred)) { return true; } - CrySLCondPredicate condNegPred = (CrySLCondPredicate) negPred; - for (StateNode s : condNegPred.getConditionalMethods()) { if (WrappedState.of(s).equals(stateNode)) { return true; @@ -859,9 +843,7 @@ public Collection computeMissingPredicates() { Collection remainingPredicates = new HashSet<>(requiredPredicates); for (ISLConstraint pred : requiredPredicates) { - if (pred instanceof RequiredCrySLPredicate) { - RequiredCrySLPredicate reqPred = (RequiredCrySLPredicate) pred; - + if (pred instanceof RequiredCrySLPredicate reqPred) { // If a negated predicate is ensured, a PredicateContradictionError has to be // reported if (reqPred.getPred().isNegated()) { @@ -879,17 +861,12 @@ public Collection computeMissingPredicates() { remainingPredicates.remove(pred); } } - } else if (pred instanceof AlternativeReqPredicate) { - AlternativeReqPredicate altPred = (AlternativeReqPredicate) pred; + } else if (pred instanceof AlternativeReqPredicate altPred) { Collection alternatives = altPred.getAlternatives(); Collection positives = - alternatives.stream() - .filter(e -> !e.isNegated()) - .collect(Collectors.toList()); + alternatives.stream().filter(e -> !e.isNegated()).toList(); Collection negatives = - alternatives.stream() - .filter(CrySLPredicate::isNegated) - .collect(Collectors.toList()); + alternatives.stream().filter(CrySLPredicate::isNegated).toList(); int altParamIndex = altPred.getParamIndex(); boolean satisfied = false; @@ -917,7 +894,7 @@ public Collection computeMissingPredicates() { e -> doReqPredAndEnsPredMatch( e, altParamIndex, ensPredAtIndex)) - .collect(Collectors.toList()); + .toList(); ensuredNegatives.removeAll(violatedNegAlternatives); } @@ -929,9 +906,7 @@ public Collection computeMissingPredicates() { // Check conditional required predicates for (ISLConstraint rem : new HashSet<>(remainingPredicates)) { - if (rem instanceof RequiredCrySLPredicate) { - RequiredCrySLPredicate singlePred = (RequiredCrySLPredicate) rem; - + if (rem instanceof RequiredCrySLPredicate singlePred) { if (isPredConditionViolated(singlePred.getPred())) { remainingPredicates.remove(singlePred); } @@ -953,9 +928,7 @@ public Collection computeContradictedPredicates() { Collection contradictedPredicates = new HashSet<>(); for (ISLConstraint pred : requiredPredicates) { - if (pred instanceof RequiredCrySLPredicate) { - RequiredCrySLPredicate reqPred = (RequiredCrySLPredicate) pred; - + if (pred instanceof RequiredCrySLPredicate reqPred) { // Only negated predicates can be contradicted if (!reqPred.getPred().isNegated()) { continue; @@ -1033,20 +1006,19 @@ private boolean doPredsMatch(CrySLPredicate pred, EnsuredCrySLPredicate ensPred) Collection expVals = Collections.emptySet(); for (CallSiteWithExtractedValue cswpi : ensPred.getParametersToValues()) { - if (cswpi.getCallSiteWithParam().getVarName().equals(parameterI)) { + if (cswpi.callSiteWithParam().varName().equals(parameterI)) { actVals = retrieveValueFromUnit(cswpi); } } for (CallSiteWithExtractedValue cswpi : constraintSolver.getCollectedValues()) { - if (cswpi.getCallSiteWithParam().getVarName().equals(var)) { + if (cswpi.callSiteWithParam().varName().equals(var)) { expVals = retrieveValueFromUnit(cswpi); } } String splitter = ""; int index = -1; - if (pred.getParameters().get(i) instanceof CrySLObject) { - CrySLObject obj = (CrySLObject) pred.getParameters().get(i); + if (pred.getParameters().get(i) instanceof CrySLObject obj) { if (obj.getSplitter() != null) { splitter = obj.getSplitter().getSplitter(); index = obj.getSplitter().getIndex(); @@ -1088,7 +1060,7 @@ && doPredsMatch(pred, hiddenPredicate)) { private Collection retrieveValueFromUnit(CallSiteWithExtractedValue callSite) { Collection values = new ArrayList<>(); - Statement statement = callSite.getCallSiteWithParam().stmt(); + Statement statement = callSite.callSiteWithParam().statement(); if (statement.isAssign()) { Val rightSide = statement.getRightOp(); @@ -1136,20 +1108,13 @@ public Map getAllCallsOnObject() { @Override public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + ((specification == null) ? 0 : specification.hashCode()); - return result; + return Objects.hash(super.hashCode(), specification); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (!super.equals(obj)) return false; - if (getClass() != obj.getClass()) return false; - AnalysisSeedWithSpecification other = (AnalysisSeedWithSpecification) obj; - if (specification == null) { - return other.specification == null; - } else return specification.equals(other.specification); + return super.equals(obj) + && obj instanceof AnalysisSeedWithSpecification other + && Objects.equals(specification, other.specification); } } diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/EnsuredCrySLPredicate.java b/CryptoAnalysis/src/main/java/crypto/analysis/EnsuredCrySLPredicate.java index 6c33d3bba..974a5b501 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/EnsuredCrySLPredicate.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/EnsuredCrySLPredicate.java @@ -3,6 +3,7 @@ import crypto.extractparameter.CallSiteWithExtractedValue; import crysl.rule.CrySLPredicate; import java.util.Collection; +import java.util.Objects; public class EnsuredCrySLPredicate { @@ -29,21 +30,11 @@ public String toString() { @Override public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((predicate == null) ? 0 : predicate.hashCode()); - return result; + return Objects.hash(predicate); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (getClass() != obj.getClass()) return false; - EnsuredCrySLPredicate other = (EnsuredCrySLPredicate) obj; - if (predicate == null) { - if (other.predicate != null) return false; - } else if (!predicate.equals(other.predicate)) return false; - return true; + return obj instanceof EnsuredCrySLPredicate other && predicate.equals(other.predicate); } } diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/IAnalysisSeed.java b/CryptoAnalysis/src/main/java/crypto/analysis/IAnalysisSeed.java index 9e6626d2b..e6750e823 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/IAnalysisSeed.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/IAnalysisSeed.java @@ -9,12 +9,9 @@ import com.google.common.collect.Multimap; import crypto.analysis.errors.AbstractError; import crysl.rule.CrySLPredicate; -import java.math.BigInteger; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; import java.util.Collection; import java.util.HashSet; +import java.util.Objects; import typestate.TransitionFunction; public abstract class IAnalysisSeed { @@ -25,55 +22,11 @@ public abstract class IAnalysisSeed { protected final Multimap expectedPredicates = HashMultimap.create(); - protected static final class ExpectedPredicateOnSeed { - - private final CrySLPredicate predicate; - private final IAnalysisSeed seed; - private final int paramIndex; - - public ExpectedPredicateOnSeed( - CrySLPredicate predicate, IAnalysisSeed seed, int paramIndex) { - this.predicate = predicate; - this.seed = seed; - this.paramIndex = paramIndex; - } - - public CrySLPredicate getPredicate() { - return predicate; - } - - public IAnalysisSeed getSeed() { - return seed; - } - - public int getParamIndex() { - return paramIndex; - } - - @Override - public int hashCode() { - return Arrays.hashCode(new Object[] {predicate, seed, paramIndex}); - } - - @Override - public boolean equals(Object obj) { - if (getClass() != obj.getClass()) return false; - ExpectedPredicateOnSeed other = (ExpectedPredicateOnSeed) obj; - - return predicate.equals(other.getPredicate()) - && seed.equals(other.getSeed()) - && paramIndex == other.getParamIndex(); - } - - @Override - public String toString() { - return predicate + " for " + seed + " @ " + paramIndex; - } - } + protected record ExpectedPredicateOnSeed( + CrySLPredicate predicate, IAnalysisSeed seed, int paramIndex) {} private final Statement origin; private final Val fact; - private String objectId; private boolean secure = true; public IAnalysisSeed( @@ -104,7 +57,7 @@ protected Collection expectedPredicatesAtStatement(Statement sta Collection expectedPredicateOnSeeds = expectedPredicates.get(statement); for (ExpectedPredicateOnSeed expectedPredicateOnSeed : expectedPredicateOnSeeds) { - predicates.add(expectedPredicateOnSeed.getPredicate()); + predicates.add(expectedPredicateOnSeed.predicate()); } return predicates; @@ -150,38 +103,16 @@ public CryptoScanner getScanner() { return scanner; } - public String getObjectId() { - if (objectId == null) { - MessageDigest md; - try { - md = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - return null; - } - this.objectId = new BigInteger(1, md.digest(this.toString().getBytes())).toString(16); - } - return this.objectId; - } - @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - - if (!(obj instanceof IAnalysisSeed)) return false; - IAnalysisSeed other = (IAnalysisSeed) obj; - - if (!origin.equals(other.getOrigin())) return false; - return fact.equals(other.getFact()); + return obj instanceof IAnalysisSeed other + && Objects.equals(origin, other.origin) + && Objects.equals(fact, other.fact); } @Override public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((origin == null) ? 0 : origin.hashCode()); - result = prime * result + ((fact == null) ? 0 : fact.hashCode()); - return result; + return Objects.hash(origin, fact); } @Override diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/RequiredCrySLPredicate.java b/CryptoAnalysis/src/main/java/crypto/analysis/RequiredCrySLPredicate.java index d5d84d2d3..f04a74e68 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/RequiredCrySLPredicate.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/RequiredCrySLPredicate.java @@ -4,6 +4,7 @@ import crysl.rule.CrySLPredicate; import crysl.rule.ISLConstraint; import java.util.List; +import java.util.Objects; public class RequiredCrySLPredicate implements ISLConstraint { @@ -19,31 +20,15 @@ public RequiredCrySLPredicate(CrySLPredicate predicate, Statement statement, int @Override public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((predicate == null) ? 0 : predicate.hashCode()); - result = prime * result + ((statement == null) ? 0 : statement.hashCode()); - result = prime * result + paramIndex; - return result; + return Objects.hash(predicate, statement, paramIndex); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (getClass() != obj.getClass()) return false; - RequiredCrySLPredicate other = (RequiredCrySLPredicate) obj; - if (predicate == null) { - if (other.predicate != null) return false; - } else if (!predicate.equals(other.predicate)) return false; - if (statement == null) { - if (other.statement != null) { - return false; - } - } else if (!statement.equals(other.statement)) { - return false; - } - return paramIndex == other.paramIndex; + return obj instanceof RequiredCrySLPredicate other + && Objects.equals(predicate, other.predicate) + && Objects.equals(statement, other.statement) + && paramIndex == other.paramIndex; } public CrySLPredicate getPred() { diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractConstraintsError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractConstraintsError.java new file mode 100644 index 000000000..d15e2907d --- /dev/null +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractConstraintsError.java @@ -0,0 +1,23 @@ +package crypto.analysis.errors; + +import boomerang.scene.Statement; +import crypto.analysis.IAnalysisSeed; +import crysl.rule.CrySLRule; + +/** Super class for all errors that violate a constraint from the CONSTRAINTS section */ +public abstract class AbstractConstraintsError extends AbstractError { + + public AbstractConstraintsError(IAnalysisSeed seed, Statement errorStmt, CrySLRule rule) { + super(seed, errorStmt, rule); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj); + } +} diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractError.java index a48aafb39..c9171f630 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractError.java @@ -5,10 +5,10 @@ import crypto.analysis.IAnalysisSeed; import crysl.rule.CrySLMethod; import crysl.rule.CrySLRule; -import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Map; +import java.util.Objects; public abstract class AbstractError { @@ -112,34 +112,14 @@ protected String formatMethodName(CrySLMethod method) { @Override public int hashCode() { - return Arrays.hashCode(new Object[] {seed, errorStmt, rule}); + return Objects.hash(seed, errorStmt, rule); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (getClass() != obj.getClass()) return false; - - AbstractError other = (AbstractError) obj; - if (seed == null) { - if (other.getSeed() != null) return false; - } else if (!seed.equals(other.getSeed())) { - return false; - } - - if (errorStmt == null) { - if (other.getErrorStatement() != null) return false; - } else if (!errorStmt.equals(other.getErrorStatement())) { - return false; - } - - if (rule == null) { - if (other.getRule() != null) return false; - } else if (!rule.equals(other.getRule())) { - return false; - } - - return true; + return obj instanceof AbstractError other + && Objects.equals(seed, other.seed) + && Objects.equals(errorStmt, other.errorStmt) + && Objects.equals(rule, other.rule); } } diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractOrderError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractOrderError.java new file mode 100644 index 000000000..a8a7d1893 --- /dev/null +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractOrderError.java @@ -0,0 +1,23 @@ +package crypto.analysis.errors; + +import boomerang.scene.Statement; +import crypto.analysis.IAnalysisSeed; +import crysl.rule.CrySLRule; + +/** Super class for all errors from the ORDER section */ +public abstract class AbstractOrderError extends AbstractError { + + public AbstractOrderError(IAnalysisSeed seed, Statement errorStmt, CrySLRule rule) { + super(seed, errorStmt, rule); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj); + } +} diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiresError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiresError.java new file mode 100644 index 000000000..353bb21bf --- /dev/null +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiresError.java @@ -0,0 +1,23 @@ +package crypto.analysis.errors; + +import boomerang.scene.Statement; +import crypto.analysis.IAnalysisSeed; +import crysl.rule.CrySLRule; + +/** Super class for all errors that violate a constraint from the REQUIRES section */ +public abstract class AbstractRequiresError extends AbstractError { + + public AbstractRequiresError(IAnalysisSeed seed, Statement errorStmt, CrySLRule rule) { + super(seed, errorStmt, rule); + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj); + } +} diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/CallToError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/CallToError.java index 58818d6fe..db46b93dc 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/CallToError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/CallToError.java @@ -3,10 +3,10 @@ import crypto.analysis.IAnalysisSeed; import crysl.rule.CrySLMethod; import crysl.rule.CrySLRule; -import java.util.Arrays; import java.util.Collection; +import java.util.Objects; -public class CallToError extends AbstractError { +public class CallToError extends AbstractConstraintsError { private final Collection requiredMethods; @@ -31,22 +31,14 @@ public String toErrorMarkerString() { @Override public int hashCode() { - return Arrays.hashCode(new Object[] {super.hashCode(), requiredMethods}); + return Objects.hash(super.hashCode(), requiredMethods); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (getClass() != obj.getClass()) return false; - - CallToError other = (CallToError) obj; - if (!super.equals(other)) return false; - if (requiredMethods == null) { - return other.getRequiredMethods() == null; - } else { - return requiredMethods.equals(other.getRequiredMethods()); - } + return super.equals(obj) + && obj instanceof CallToError other + && Objects.equals(requiredMethods, other.getRequiredMethods()); } @Override diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/ConstraintError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/ConstraintError.java index 2017362cb..27f2ee185 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/ConstraintError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/ConstraintError.java @@ -13,10 +13,10 @@ import crysl.rule.CrySLSplitter; import crysl.rule.CrySLValueConstraint; import crysl.rule.ISLConstraint; -import java.util.Arrays; import java.util.Collection; +import java.util.Objects; -public class ConstraintError extends AbstractError { +public class ConstraintError extends AbstractConstraintsError { private final CallSiteWithExtractedValue callSite; private final ISLConstraint violatedConstraint; @@ -26,7 +26,7 @@ public ConstraintError( CallSiteWithExtractedValue cs, CrySLRule rule, ISLConstraint constraint) { - super(seed, cs.getCallSiteWithParam().stmt(), rule); + super(seed, cs.callSiteWithParam().statement(), rule); this.callSite = cs; this.violatedConstraint = constraint; @@ -49,24 +49,20 @@ private String evaluateBrokenConstraint(final ISLConstraint constraint) { StringBuilder msg = new StringBuilder(); if (constraint instanceof CrySLValueConstraint) { return evaluateValueConstraint((CrySLValueConstraint) constraint); - } else if (constraint instanceof CrySLArithmeticConstraint) { - final CrySLArithmeticConstraint brokenArthConstraint = - (CrySLArithmeticConstraint) constraint; - msg.append(brokenArthConstraint.getLeft()); + } else if (constraint instanceof CrySLArithmeticConstraint brokenArithConstraint) { + msg.append(brokenArithConstraint.getLeft()); msg.append(" "); - msg.append(brokenArthConstraint.getOperator()); + msg.append(brokenArithConstraint.getOperator()); msg.append(" "); - msg.append(brokenArthConstraint.getRight()); - } else if (constraint instanceof CrySLComparisonConstraint) { - final CrySLComparisonConstraint brokenCompCons = (CrySLComparisonConstraint) constraint; + msg.append(brokenArithConstraint.getRight()); + } else if (constraint instanceof CrySLComparisonConstraint brokenCompCons) { msg.append(" Variable "); msg.append(brokenCompCons.getLeft().getLeft().getName()); msg.append(" must be "); msg.append(evaluateCompOp(brokenCompCons.getOperator())); msg.append(" "); msg.append(brokenCompCons.getRight().getLeft().getName()); - } else if (constraint instanceof CrySLConstraint) { - final CrySLConstraint crySLConstraint = (CrySLConstraint) constraint; + } else if (constraint instanceof CrySLConstraint crySLConstraint) { final ISLConstraint leftSide = crySLConstraint.getLeft(); final ISLConstraint rightSide = crySLConstraint.getRight(); switch (crySLConstraint.getOperator()) { @@ -91,21 +87,14 @@ private String evaluateBrokenConstraint(final ISLConstraint constraint) { } private String evaluateCompOp(CrySLComparisonConstraint.CompOp operator) { - switch (operator) { - case ge: - return "at least"; - case g: - return "greater than"; - case l: - return "lesser than"; - case le: - return "at most"; - case eq: - return "equal to"; - case neq: - return "not equal to"; - } - return ""; + return switch (operator) { + case ge -> "at least"; + case g -> "greater than"; + case l -> "lesser than"; + case le -> "at most"; + case eq -> "equal to"; + case neq -> "not equal to"; + }; } private String evaluateValueConstraint(final CrySLValueConstraint brokenConstraint) { @@ -113,7 +102,7 @@ private String evaluateValueConstraint(final CrySLValueConstraint brokenConstrai msg.append(" should be any of "); CrySLSplitter splitter = brokenConstraint.getVar().getSplitter(); if (splitter != null) { - Statement stmt = callSite.getCallSiteWithParam().stmt(); + Statement stmt = callSite.callSiteWithParam().statement(); String[] splitValues = new String[] {""}; if (stmt.isAssign()) { Val rightSide = stmt.getRightOp(); @@ -135,10 +124,8 @@ private String evaluateValueConstraint(final CrySLValueConstraint brokenConstrai } } } - } else { - // splitValues = - // filterQuotes(stmt.getInvokeExpr().getUseBoxes().get(0).getValue().toString()).split(splitter.getSplitter()); } + if (splitValues.length >= splitter.getIndex()) { for (int i = 0; i < splitter.getIndex(); i++) { msg.append(splitValues[i]); @@ -165,29 +152,15 @@ public static String filterQuotes(final String dirty) { @Override public int hashCode() { - return Arrays.hashCode(new Object[] {super.hashCode(), callSite, violatedConstraint}); + return Objects.hash(super.hashCode(), callSite, violatedConstraint); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (!super.equals(obj)) return false; - if (getClass() != obj.getClass()) return false; - - ConstraintError other = (ConstraintError) obj; - if (callSite == null) { - if (other.getCallSiteWithExtractedValue() != null) return false; - } else if (!callSite.equals(other.getCallSiteWithExtractedValue())) { - return false; - } - - if (violatedConstraint == null) { - if (other.getViolatedConstraint() != null) return false; - } else if (!violatedConstraint.equals(other.getViolatedConstraint())) { - return false; - } - - return true; + return super.equals(obj) + && obj instanceof ConstraintError other + && Objects.equals(callSite, other.getCallSiteWithExtractedValue()) + && Objects.equals(violatedConstraint, other.getViolatedConstraint()); } @Override diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/ForbiddenMethodError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/ForbiddenMethodError.java index 6b497c807..b6f25545d 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/ForbiddenMethodError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/ForbiddenMethodError.java @@ -5,23 +5,14 @@ import crypto.analysis.IAnalysisSeed; import crysl.rule.CrySLMethod; import crysl.rule.CrySLRule; -import java.util.Arrays; import java.util.Collection; -import java.util.HashSet; +import java.util.Objects; public class ForbiddenMethodError extends AbstractError { private final DeclaredMethod calledMethod; private final Collection alternatives; - public ForbiddenMethodError( - IAnalysisSeed seed, - Statement errorLocation, - CrySLRule rule, - DeclaredMethod calledMethod) { - this(seed, errorLocation, rule, calledMethod, new HashSet<>()); - } - public ForbiddenMethodError( IAnalysisSeed seed, Statement errorLocation, @@ -60,29 +51,15 @@ public String toErrorMarkerString() { @Override public int hashCode() { - return Arrays.hashCode(new Object[] {super.hashCode(), calledMethod, alternatives}); + return Objects.hash(super.hashCode(), calledMethod, alternatives); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (!super.equals(obj)) return false; - if (getClass() != obj.getClass()) return false; - - ForbiddenMethodError other = (ForbiddenMethodError) obj; - if (calledMethod == null) { - if (other.getCalledMethod() != null) return false; - } else if (!calledMethod.equals(other.getCalledMethod())) { - return false; - } - - if (alternatives == null) { - if (other.getAlternatives() != null) return false; - } else if (!alternatives.equals(other.getAlternatives())) { - return false; - } - - return true; + return super.equals(obj) + && obj instanceof ForbiddenMethodError other + && Objects.equals(calledMethod, other.getCalledMethod()) + && Objects.equals(alternatives, other.getAlternatives()); } @Override diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/HardCodedError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/HardCodedError.java index 452978b1d..db1715fea 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/HardCodedError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/HardCodedError.java @@ -4,9 +4,9 @@ import crypto.extractparameter.CallSiteWithExtractedValue; import crysl.rule.CrySLRule; import crysl.rule.ISLConstraint; -import java.util.Arrays; +import java.util.Objects; -public class HardCodedError extends AbstractError { +public class HardCodedError extends AbstractConstraintsError { private final CallSiteWithExtractedValue extractedValue; private final ISLConstraint violatedConstraint; @@ -16,7 +16,7 @@ public HardCodedError( CallSiteWithExtractedValue cs, CrySLRule rule, ISLConstraint constraint) { - super(seed, cs.getCallSiteWithParam().stmt(), rule); + super(seed, cs.callSiteWithParam().statement(), rule); this.extractedValue = cs; this.violatedConstraint = constraint; @@ -37,29 +37,15 @@ public String toErrorMarkerString() { @Override public int hashCode() { - return Arrays.hashCode(new Object[] {super.hashCode(), extractedValue, violatedConstraint}); + return Objects.hash(super.hashCode(), extractedValue, violatedConstraint); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (!super.equals(obj)) return false; - if (getClass() != obj.getClass()) return false; - - HardCodedError other = (HardCodedError) obj; - if (extractedValue == null) { - if (other.getExtractedValue() != null) return false; - } else if (!extractedValue.equals(other.getExtractedValue())) { - return false; - } - - if (violatedConstraint == null) { - if (other.getViolatedConstraint() != null) return false; - } else if (!violatedConstraint.equals(other.getViolatedConstraint())) { - return false; - } - - return true; + return super.equals(obj) + && obj instanceof HardCodedError other + && Objects.equals(extractedValue, other.getExtractedValue()) + && Objects.equals(violatedConstraint, other.getViolatedConstraint()); } @Override diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/ImpreciseValueExtractionError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/ImpreciseValueExtractionError.java index 1dbc26a87..fadfdda96 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/ImpreciseValueExtractionError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/ImpreciseValueExtractionError.java @@ -4,9 +4,9 @@ import crypto.analysis.IAnalysisSeed; import crysl.rule.CrySLRule; import crysl.rule.ISLConstraint; -import java.util.Arrays; +import java.util.Objects; -public class ImpreciseValueExtractionError extends AbstractError { +public class ImpreciseValueExtractionError extends AbstractConstraintsError { private final ISLConstraint violatedConstraint; @@ -29,23 +29,14 @@ public String toErrorMarkerString() { @Override public int hashCode() { - return Arrays.hashCode(new Object[] {super.hashCode(), violatedConstraint}); + return Objects.hash(super.hashCode(), violatedConstraint); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (!super.equals(obj)) return false; - if (getClass() != obj.getClass()) return false; - - ImpreciseValueExtractionError other = (ImpreciseValueExtractionError) obj; - if (violatedConstraint == null) { - if (other.violatedConstraint != null) return false; - } else if (!violatedConstraint.equals(other.violatedConstraint)) { - return false; - } - - return true; + return super.equals(obj) + && obj instanceof ImpreciseValueExtractionError other + && Objects.equals(violatedConstraint, other.getViolatedConstraint()); } @Override diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/IncompleteOperationError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/IncompleteOperationError.java index 4d59f9ecd..2f48ec82f 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/IncompleteOperationError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/IncompleteOperationError.java @@ -5,8 +5,8 @@ import crypto.analysis.IAnalysisSeed; import crysl.rule.CrySLMethod; import crysl.rule.CrySLRule; -import java.util.Arrays; import java.util.Collection; +import java.util.Objects; /** * This class defines-IncompleteOperationError: @@ -19,7 +19,7 @@ * decryption, this may render the code dead. This error heavily depends on the computed call graph * (CHA by default). */ -public class IncompleteOperationError extends AbstractError { +public class IncompleteOperationError extends AbstractOrderError { private final Collection expectedMethodCalls; private final boolean multiplePaths; @@ -112,23 +112,15 @@ private String getErrorMarkerStringForMultipleDataflowPaths() { @Override public int hashCode() { - return Arrays.hashCode(new Object[] {super.hashCode(), expectedMethodCalls, multiplePaths}); + return Objects.hash(super.hashCode(), expectedMethodCalls, multiplePaths); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (!super.equals(obj)) return false; - if (getClass() != obj.getClass()) return false; - - IncompleteOperationError other = (IncompleteOperationError) obj; - if (expectedMethodCalls == null) { - if (other.getExpectedMethodCalls() != null) return false; - } else if (expectedMethodCalls != other.getExpectedMethodCalls()) { - return false; - } - - return multiplePaths == other.isMultiplePaths(); + return super.equals(obj) + && obj instanceof IncompleteOperationError other + && Objects.equals(expectedMethodCalls, other.getExpectedMethodCalls()) + && multiplePaths == other.isMultiplePaths(); } @Override diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/InstanceOfError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/InstanceOfError.java index a558a5ec9..c34e4a6b4 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/InstanceOfError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/InstanceOfError.java @@ -6,9 +6,9 @@ import crysl.rule.CrySLPredicate; import crysl.rule.CrySLRule; import crysl.rule.ISLConstraint; -import java.util.Arrays; +import java.util.Objects; -public class InstanceOfError extends AbstractError { +public class InstanceOfError extends AbstractConstraintsError { private final CallSiteWithExtractedValue extractedValue; private final CrySLPredicate violatedConstraint; @@ -18,7 +18,7 @@ public InstanceOfError( CallSiteWithExtractedValue cs, CrySLRule rule, CrySLPredicate constraint) { - super(seed, cs.getCallSiteWithParam().stmt(), rule); + super(seed, cs.callSiteWithParam().statement(), rule); this.extractedValue = cs; this.violatedConstraint = constraint; @@ -41,29 +41,15 @@ public String toErrorMarkerString() { @Override public int hashCode() { - return Arrays.hashCode(new Object[] {super.hashCode(), extractedValue, violatedConstraint}); + return Objects.hash(super.hashCode(), extractedValue, violatedConstraint); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (!super.equals(obj)) return false; - if (getClass() != obj.getClass()) return false; - - InstanceOfError other = (InstanceOfError) obj; - if (extractedValue == null) { - if (other.getExtractedValue() != null) return false; - } else if (!extractedValue.equals(other.getExtractedValue())) { - return false; - } - - if (violatedConstraint == null) { - if (other.getViolatedConstraint() != null) return false; - } else if (!violatedConstraint.equals(other.getViolatedConstraint())) { - return false; - } - - return true; + return super.equals(obj) + && obj instanceof InstanceOfError other + && Objects.equals(extractedValue, other.getExtractedValue()) + && Objects.equals(violatedConstraint, other.getViolatedConstraint()); } @Override diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/NeverTypeOfError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/NeverTypeOfError.java index e1bc9b033..0eb5e2b0e 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/NeverTypeOfError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/NeverTypeOfError.java @@ -6,9 +6,9 @@ import crysl.rule.CrySLPredicate; import crysl.rule.CrySLRule; import crysl.rule.ISLConstraint; -import java.util.Arrays; +import java.util.Objects; -public class NeverTypeOfError extends AbstractError { +public class NeverTypeOfError extends AbstractConstraintsError { private final CallSiteWithExtractedValue extractedValue; private final CrySLPredicate violatedConstraint; @@ -18,7 +18,7 @@ public NeverTypeOfError( CallSiteWithExtractedValue cs, CrySLRule rule, CrySLPredicate constraint) { - super(seed, cs.getCallSiteWithParam().stmt(), rule); + super(seed, cs.callSiteWithParam().statement(), rule); this.extractedValue = cs; this.violatedConstraint = constraint; @@ -41,29 +41,15 @@ public String toErrorMarkerString() { @Override public int hashCode() { - return Arrays.hashCode(new Object[] {super.hashCode(), extractedValue, violatedConstraint}); + return Objects.hash(super.hashCode(), extractedValue, violatedConstraint); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (!super.equals(obj)) return false; - if (getClass() != obj.getClass()) return false; - - NeverTypeOfError other = (NeverTypeOfError) obj; - if (extractedValue == null) { - if (other.getExtractedValue() != null) return false; - } else if (!extractedValue.equals(other.getExtractedValue())) { - return false; - } - - if (violatedConstraint == null) { - if (other.getViolatedConstraint() != null) return false; - } else if (!violatedConstraint.equals(other.getViolatedConstraint())) { - return false; - } - - return true; + return super.equals(obj) + && obj instanceof NeverTypeOfError other + && Objects.equals(extractedValue, other.getExtractedValue()) + && Objects.equals(violatedConstraint, other.getViolatedConstraint()); } @Override diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/NoCallToError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/NoCallToError.java index 5db72f317..53e69930e 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/NoCallToError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/NoCallToError.java @@ -3,8 +3,9 @@ import boomerang.scene.Statement; import crypto.analysis.IAnalysisSeed; import crysl.rule.CrySLRule; +import java.util.Objects; -public class NoCallToError extends AbstractError { +public class NoCallToError extends AbstractConstraintsError { public NoCallToError(IAnalysisSeed seed, Statement statement, CrySLRule rule) { super(seed, statement, rule); @@ -17,12 +18,12 @@ public String toErrorMarkerString() { @Override public int hashCode() { - return super.hashCode(); + return Objects.hash(super.hashCode()); } @Override public boolean equals(Object obj) { - return super.equals(obj); + return super.equals(obj) && obj instanceof NoCallToError; } @Override diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/PredicateContradictionError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/PredicateContradictionError.java index 5cf2f60f7..712dc2c9b 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/PredicateContradictionError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/PredicateContradictionError.java @@ -4,9 +4,9 @@ import crypto.analysis.IAnalysisSeed; import crysl.rule.CrySLPredicate; import crysl.rule.CrySLRule; -import java.util.Arrays; +import java.util.Objects; -public class PredicateContradictionError extends AbstractError { +public class PredicateContradictionError extends AbstractRequiresError { private final CrySLPredicate contradictedPredicate; @@ -31,23 +31,14 @@ public String toErrorMarkerString() { @Override public int hashCode() { - return Arrays.hashCode(new Object[] {super.hashCode(), contradictedPredicate}); + return Objects.hash(super.hashCode(), contradictedPredicate); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (!super.equals(obj)) return false; - if (getClass() != obj.getClass()) return false; - - PredicateContradictionError other = (PredicateContradictionError) obj; - if (contradictedPredicate == null) { - if (other.getContradictedPredicate() != null) return false; - } else if (!contradictedPredicate.equals(other.getContradictedPredicate())) { - return false; - } - - return true; + return super.equals(obj) + && obj instanceof PredicateContradictionError other + && Objects.equals(contradictedPredicate, other.getContradictedPredicate()); } @Override diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java index 5cee34c21..941b44d02 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java @@ -15,7 +15,7 @@ * Creates {@link RequiredPredicateError} for all Required Predicate error generates * RequiredPredicateError */ -public class RequiredPredicateError extends AbstractError { +public class RequiredPredicateError extends AbstractRequiresError { private final Collection hiddenPredicates; private final Collection contradictedPredicates; diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/TypestateError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/TypestateError.java index 632a0bd33..750095f91 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/TypestateError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/TypestateError.java @@ -4,10 +4,10 @@ import crypto.analysis.IAnalysisSeed; import crysl.rule.CrySLMethod; import crysl.rule.CrySLRule; -import java.util.Arrays; import java.util.Collection; +import java.util.Objects; -public class TypestateError extends AbstractError { +public class TypestateError extends AbstractOrderError { private final Collection expectedMethodCalls; @@ -44,23 +44,14 @@ public String toErrorMarkerString() { @Override public int hashCode() { - return Arrays.hashCode(new Object[] {super.hashCode(), expectedMethodCalls}); + return Objects.hash(super.hashCode(), expectedMethodCalls); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (!super.equals(obj)) return false; - if (getClass() != obj.getClass()) return false; - - TypestateError other = (TypestateError) obj; - if (expectedMethodCalls == null) { - if (other.getExpectedMethodCalls() != null) return false; - } else if (!expectedMethodCalls.equals(other.getExpectedMethodCalls())) { - return false; - } - - return true; + return super.equals(obj) + && obj instanceof TypestateError other + && Objects.equals(expectedMethodCalls, other.getExpectedMethodCalls()); } @Override diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/UncaughtExceptionError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/UncaughtExceptionError.java index 31b57b360..bf98725a5 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/UncaughtExceptionError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/UncaughtExceptionError.java @@ -4,7 +4,7 @@ import boomerang.scene.WrappedClass; import crypto.analysis.IAnalysisSeed; import crysl.rule.CrySLRule; -import java.util.Arrays; +import java.util.Objects; public class UncaughtExceptionError extends AbstractError { @@ -27,22 +27,13 @@ public String toErrorMarkerString() { @Override public int hashCode() { - return Arrays.hashCode(new Object[] {super.hashCode(), exception}); + return Objects.hash(super.hashCode(), exception); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (!super.equals(obj)) return false; - if (getClass() != obj.getClass()) return false; - - UncaughtExceptionError other = (UncaughtExceptionError) obj; - if (exception == null) { - if (other.getException() != null) return false; - } else if (!exception.equals(other.getException())) { - return false; - } - - return true; + return super.equals(obj) + && obj instanceof UncaughtExceptionError other + && Objects.equals(exception, other.getException()); } } diff --git a/CryptoAnalysis/src/main/java/crypto/constraints/ComparisonConstraint.java b/CryptoAnalysis/src/main/java/crypto/constraints/ComparisonConstraint.java index 95d137ecc..54e914ad3 100644 --- a/CryptoAnalysis/src/main/java/crypto/constraints/ComparisonConstraint.java +++ b/CryptoAnalysis/src/main/java/crypto/constraints/ComparisonConstraint.java @@ -183,8 +183,8 @@ private Map extractValueAsInt( try { for (Map.Entry value : valueCollection.entrySet()) { - ExtractedValue extractedValue = value.getValue().getExtractedValue(); - if (extractedValue.getVal().equals(Val.zero())) { + ExtractedValue extractedValue = value.getValue().extractedValue(); + if (extractedValue.val().equals(Val.zero())) { continue; } diff --git a/CryptoAnalysis/src/main/java/crypto/constraints/ConstraintSolver.java b/CryptoAnalysis/src/main/java/crypto/constraints/ConstraintSolver.java index 226ecaa03..39b13d40e 100644 --- a/CryptoAnalysis/src/main/java/crypto/constraints/ConstraintSolver.java +++ b/CryptoAnalysis/src/main/java/crypto/constraints/ConstraintSolver.java @@ -153,8 +153,8 @@ private void partitionConstraints() { Collection involvedVarNames = new HashSet<>(cons.getInvolvedVarNames()); for (CallSiteWithExtractedValue callSite : this.getCollectedValues()) { - CallSiteWithParamIndex callSiteWithParamIndex = callSite.getCallSiteWithParam(); - involvedVarNames.remove(callSiteWithParamIndex.getVarName()); + CallSiteWithParamIndex callSiteWithParamIndex = callSite.callSiteWithParam(); + involvedVarNames.remove(callSiteWithParamIndex.varName()); } if (!involvedVarNames.isEmpty()) { @@ -201,15 +201,15 @@ private Collection collectAlternativePredicates( if (alts.isEmpty()) { for (CallSiteWithExtractedValue callSite : this.getCollectedValues()) { - CallSiteWithParamIndex cwpi = callSite.getCallSiteWithParam(); + CallSiteWithParamIndex cwpi = callSite.callSiteWithParam(); for (ICrySLPredicateParameter p : left.getParameters()) { if (p.getName().equals("transformation")) { continue; } - if (cwpi.getVarName().equals(p.getName())) { - alts.add(new AlternativeReqPredicate(left, cwpi.stmt(), cwpi.getIndex())); + if (cwpi.varName().equals(p.getName())) { + alts.add(new AlternativeReqPredicate(left, cwpi.statement(), cwpi.index())); } } } @@ -244,7 +244,7 @@ private Collection retrieveValuesForPred(CrySLPredicate Collection result = Lists.newArrayList(); for (CallSiteWithExtractedValue callSite : this.getCollectedValues()) { - CallSiteWithParamIndex cwpi = callSite.getCallSiteWithParam(); + CallSiteWithParamIndex cwpi = callSite.callSiteWithParam(); for (ICrySLPredicateParameter p : pred.getParameters()) { // TODO: FIX Cipher rule @@ -253,12 +253,12 @@ private Collection retrieveValuesForPred(CrySLPredicate } // Predicates with _ can have any type - if (cwpi.getVarName().equals("_")) { + if (cwpi.varName().equals("_")) { continue; } - if (cwpi.getVarName().equals(p.getName())) { - result.add(new RequiredCrySLPredicate(pred, cwpi.stmt(), cwpi.getIndex())); + if (cwpi.varName().equals(p.getName())) { + result.add(new RequiredCrySLPredicate(pred, cwpi.statement(), cwpi.index())); } } } diff --git a/CryptoAnalysis/src/main/java/crypto/constraints/EvaluableConstraint.java b/CryptoAnalysis/src/main/java/crypto/constraints/EvaluableConstraint.java index 9cf077312..84da0248f 100644 --- a/CryptoAnalysis/src/main/java/crypto/constraints/EvaluableConstraint.java +++ b/CryptoAnalysis/src/main/java/crypto/constraints/EvaluableConstraint.java @@ -75,25 +75,25 @@ protected Collection getErrors() { protected Map extractValueAsString(String varName) { Map varVal = Maps.newHashMap(); for (CallSiteWithExtractedValue callSite : context.getCollectedValues()) { - CallSiteWithParamIndex wrappedCallSite = callSite.getCallSiteWithParam(); - Statement statement = wrappedCallSite.stmt(); + CallSiteWithParamIndex wrappedCallSite = callSite.callSiteWithParam(); + Statement statement = wrappedCallSite.statement(); - if (!wrappedCallSite.getVarName().equals(varName)) { + if (!wrappedCallSite.varName().equals(varName)) { continue; } - ExtractedValue extractedValue = callSite.getExtractedValue(); - Statement allocSite = extractedValue.getInitialStatement(); + ExtractedValue extractedValue = callSite.extractedValue(); + Statement allocSite = extractedValue.initialStatement(); InvokeExpr invoker = statement.getInvokeExpr(); if (statement.equals(allocSite)) { String constant = - retrieveConstantFromValue(invoker.getArg(wrappedCallSite.getIndex())); + retrieveConstantFromValue(invoker.getArg(wrappedCallSite.index())); varVal.put(constant, callSite); } else if (allocSite.isAssign()) { - if (extractedValue.getVal().isConstant()) { + if (extractedValue.val().isConstant()) { String retrieveConstantFromValue = - retrieveConstantFromValue(extractedValue.getVal()); + retrieveConstantFromValue(extractedValue.val()); int pos = -1; for (int i = 0; i < invoker.getArgs().size(); i++) { @@ -114,7 +114,7 @@ protected Map extractValueAsString(String va retrieveConstantFromValue, new CallSiteWithExtractedValue(wrappedCallSite, extractedValue)); } - } else if (extractedValue.getVal().isNewExpr()) { + } else if (extractedValue.val().isNewExpr()) { varVal.putAll(extractSootArray(wrappedCallSite, extractedValue)); } } @@ -131,8 +131,8 @@ protected Map extractValueAsString(String va */ protected Map extractSootArray( CallSiteWithParamIndex callSite, ExtractedValue allocSite) { - Val arrayLocal = allocSite.getVal(); - Method method = callSite.stmt().getMethod(); + Val arrayLocal = allocSite.val(); + Method method = callSite.statement().getMethod(); Map arrVal = Maps.newHashMap(); @@ -186,7 +186,7 @@ private String retrieveConstantFromValue(Val val) { protected Map extractArray(ExtractedValue extractedValue) { Map result = new HashMap<>(); - Statement statement = extractedValue.getInitialStatement(); + Statement statement = extractedValue.initialStatement(); if (!statement.isAssign()) { return result; } @@ -269,8 +269,8 @@ protected boolean couldNotExtractValues( } for (CallSiteWithExtractedValue callSite : extractedValueMap.values()) { - Statement statement = callSite.getCallSiteWithParam().stmt(); - Val extractedVal = callSite.getExtractedValue().getVal(); + Statement statement = callSite.callSiteWithParam().statement(); + Val extractedVal = callSite.extractedValue().val(); if (extractedVal.equals(Val.zero())) { ImpreciseValueExtractionError extractionError = diff --git a/CryptoAnalysis/src/main/java/crypto/constraints/PredicateConstraint.java b/CryptoAnalysis/src/main/java/crypto/constraints/PredicateConstraint.java index a5e4e8c3f..dc0cebaa6 100644 --- a/CryptoAnalysis/src/main/java/crypto/constraints/PredicateConstraint.java +++ b/CryptoAnalysis/src/main/java/crypto/constraints/PredicateConstraint.java @@ -123,13 +123,13 @@ private void evaluateNeverTypeOfPredicate(CrySLPredicate neverTypeOfPredicate) { CrySLObject parameterType = objects.get(1); for (CallSiteWithExtractedValue callSite : context.getCollectedValues()) { - CallSiteWithParamIndex cs = callSite.getCallSiteWithParam(); + CallSiteWithParamIndex cs = callSite.callSiteWithParam(); - if (!variable.getName().equals(cs.getVarName())) { + if (!variable.getName().equals(cs.varName())) { continue; } - Collection types = callSite.getExtractedValue().getTypes(); + Collection types = callSite.extractedValue().types(); for (Type type : types) { if (!parameterType.getJavaType().equals(type.toString())) { continue; @@ -157,13 +157,13 @@ private void evaluateHardCodedPredicate(CrySLPredicate hardCodedPredicate) { CrySLObject variable = objects.get(0); for (CallSiteWithExtractedValue callSite : context.getCollectedValues()) { - CallSiteWithParamIndex cs = callSite.getCallSiteWithParam(); + CallSiteWithParamIndex cs = callSite.callSiteWithParam(); - if (!variable.getVarName().equals(cs.getVarName())) { + if (!variable.getVarName().equals(cs.varName())) { continue; } - ExtractedValue extractedValue = callSite.getExtractedValue(); + ExtractedValue extractedValue = callSite.extractedValue(); if (isHardCodedVariable(extractedValue) || isHardCodedArray(extractedValue)) { HardCodedError hardCodedError = new HardCodedError( @@ -188,14 +188,14 @@ private void evaluateInstanceOfPredicate(CrySLPredicate instanceOfPredicate) { CrySLObject parameterType = objects.get(1); for (CallSiteWithExtractedValue callSite : context.getCollectedValues()) { - CallSiteWithParamIndex cs = callSite.getCallSiteWithParam(); + CallSiteWithParamIndex cs = callSite.callSiteWithParam(); - if (!variable.getName().equals(cs.getVarName())) { + if (!variable.getName().equals(cs.varName())) { continue; } boolean isSubType = false; - Collection types = callSite.getExtractedValue().getTypes(); + Collection types = callSite.extractedValue().types(); for (Type type : types) { if (type.isNullType()) { continue; @@ -250,26 +250,26 @@ private List parametersToCryslObjects( public boolean isHardCodedVariable(ExtractedValue val) { // Check for basic constants - if (val.getVal().isConstant()) { - LOGGER.debug("Value {} is hard coded", val.getVal()); + if (val.val().isConstant()) { + LOGGER.debug("Value {} is hard coded", val.val()); return true; } - Statement statement = val.getInitialStatement(); + Statement statement = val.initialStatement(); // Objects initialized with 'new' are hard coded if (!statement.isAssign()) { - LOGGER.debug("Value {} is not hard coded", val.getVal()); + LOGGER.debug("Value {} is not hard coded", val.val()); return false; } Val rightOp = statement.getRightOp(); if (rightOp.isNewExpr()) { - LOGGER.debug("Value {} is hard coded", val.getVal()); + LOGGER.debug("Value {} is hard coded", val.val()); return true; } - LOGGER.debug("Value {} is not hard coded", val.getVal()); + LOGGER.debug("Value {} is not hard coded", val.val()); return false; } diff --git a/CryptoAnalysis/src/main/java/crypto/extractparameter/CallSiteWithExtractedValue.java b/CryptoAnalysis/src/main/java/crypto/extractparameter/CallSiteWithExtractedValue.java index fe26c6ead..59debc10a 100644 --- a/CryptoAnalysis/src/main/java/crypto/extractparameter/CallSiteWithExtractedValue.java +++ b/CryptoAnalysis/src/main/java/crypto/extractparameter/CallSiteWithExtractedValue.java @@ -1,31 +1,14 @@ package crypto.extractparameter; import boomerang.scene.Val; -import java.util.Arrays; -public class CallSiteWithExtractedValue { - - private final CallSiteWithParamIndex callSiteWithParam; - private final ExtractedValue extractedValue; - - public CallSiteWithExtractedValue( - CallSiteWithParamIndex callSiteWithParam, ExtractedValue extractedValue) { - this.callSiteWithParam = callSiteWithParam; - this.extractedValue = extractedValue; - } - - public CallSiteWithParamIndex getCallSiteWithParam() { - return callSiteWithParam; - } - - public ExtractedValue getExtractedValue() { - return extractedValue; - } +public record CallSiteWithExtractedValue( + CallSiteWithParamIndex callSiteWithParam, ExtractedValue extractedValue) { @Override public String toString() { String res; - switch (callSiteWithParam.getIndex()) { + switch (callSiteWithParam.index()) { case -1: return "Return value"; case 0: @@ -47,12 +30,12 @@ public String toString() { res = "Sixth "; break; default: - res = (callSiteWithParam.getIndex() + 1) + "th "; + res = (callSiteWithParam.index() + 1) + "th "; break; } res += "parameter"; if (extractedValue != null) { - Val allocVal = extractedValue.getVal(); + Val allocVal = extractedValue.val(); if (allocVal.isConstant()) { res += " (with value " + allocVal.getVariableName() + ")"; @@ -60,31 +43,4 @@ public String toString() { } return res; } - - @Override - public int hashCode() { - return Arrays.hashCode(new Object[] {callSiteWithParam, extractedValue}); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (getClass() != obj.getClass()) return false; - - CallSiteWithExtractedValue other = (CallSiteWithExtractedValue) obj; - if (callSiteWithParam == null) { - if (other.getCallSiteWithParam() != null) return false; - } else if (!callSiteWithParam.equals(other.getCallSiteWithParam())) { - return false; - } - - if (extractedValue == null) { - if (other.getExtractedValue() != null) return false; - } else if (!extractedValue.equals(other.getExtractedValue())) { - return false; - } - - return true; - } } diff --git a/CryptoAnalysis/src/main/java/crypto/extractparameter/CallSiteWithParamIndex.java b/CryptoAnalysis/src/main/java/crypto/extractparameter/CallSiteWithParamIndex.java index 64646bbac..27e9a9d32 100644 --- a/CryptoAnalysis/src/main/java/crypto/extractparameter/CallSiteWithParamIndex.java +++ b/CryptoAnalysis/src/main/java/crypto/extractparameter/CallSiteWithParamIndex.java @@ -1,65 +1,11 @@ package crypto.extractparameter; import boomerang.scene.Statement; -import boomerang.scene.Val; -public class CallSiteWithParamIndex { - - private final Statement statement; - private final Val fact; - private final String varName; - private final int index; - - public CallSiteWithParamIndex(Statement statement, Val fact, int index, String varName) { - this.statement = statement; - this.fact = fact; - this.index = index; - this.varName = varName; - } - - public Statement stmt() { - return statement; - } - - public Val fact() { - return fact; - } - - public int getIndex() { - return index; - } - - public String getVarName() { - return varName; - } +public record CallSiteWithParamIndex(Statement statement, int index, String varName) { @Override public String toString() { - return varName + " at " + stmt() + " and " + index; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + index; - result = prime * result + ((statement == null) ? 0 : statement.hashCode()); - result = prime * result + ((varName == null) ? 0 : varName.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (getClass() != obj.getClass()) return false; - CallSiteWithParamIndex other = (CallSiteWithParamIndex) obj; - if (index != other.index) return false; - if (statement == null) { - if (other.statement != null) return false; - } else if (!statement.equals(other.statement)) return false; - if (varName == null) { - return other.varName == null; - } else return varName.equals(other.varName); + return varName + " at " + statement + " and " + index; } } diff --git a/CryptoAnalysis/src/main/java/crypto/extractparameter/ExtractParameterAnalysis.java b/CryptoAnalysis/src/main/java/crypto/extractparameter/ExtractParameterAnalysis.java index 3bb54bc2a..940f23d7d 100644 --- a/CryptoAnalysis/src/main/java/crypto/extractparameter/ExtractParameterAnalysis.java +++ b/CryptoAnalysis/src/main/java/crypto/extractparameter/ExtractParameterAnalysis.java @@ -78,9 +78,7 @@ private void addQueryAtCallSite(Statement statement, String varNameInSpec, int i for (ForwardQuery paramQuery : filteredQueries) { Val val = paramQuery.var(); - if (val instanceof AllocVal) { - AllocVal allocVal = (AllocVal) val; - + if (val instanceof AllocVal allocVal) { Map.Entry entry = new AbstractMap.SimpleEntry<>( allocVal.getAllocVal(), @@ -95,8 +93,7 @@ private void addQueryAtCallSite(Statement statement, String varNameInSpec, int i } CallSiteWithParamIndex callSiteWithParam = - new CallSiteWithParamIndex( - statement, parameter, index, varNameInSpec); + new CallSiteWithParamIndex(statement, index, varNameInSpec); Collection types = results.getPropagationType(); // If no value could be extracted, add the zero value to indicate it diff --git a/CryptoAnalysis/src/main/java/crypto/extractparameter/ExtractParameterQuery.java b/CryptoAnalysis/src/main/java/crypto/extractparameter/ExtractParameterQuery.java index 04a6dfea1..c09dbddfd 100644 --- a/CryptoAnalysis/src/main/java/crypto/extractparameter/ExtractParameterQuery.java +++ b/CryptoAnalysis/src/main/java/crypto/extractparameter/ExtractParameterQuery.java @@ -5,9 +5,9 @@ import boomerang.results.BackwardBoomerangResults; import boomerang.scene.ControlFlowGraph; import boomerang.scene.Val; -import java.util.Arrays; import java.util.Collection; import java.util.HashSet; +import java.util.Objects; import wpds.impl.Weight; public class ExtractParameterQuery extends BackwardQuery { @@ -62,15 +62,13 @@ public String toString() { @Override public int hashCode() { - return Arrays.hashCode(new Object[] {super.hashCode(), index}); + return Objects.hash(super.hashCode(), index); } @Override public boolean equals(Object obj) { - if (!super.equals(obj)) return false; - if (getClass() != obj.getClass()) return false; - - ExtractParameterQuery other = (ExtractParameterQuery) obj; - return index == other.index; + return super.equals(obj) + && obj instanceof ExtractParameterQuery other + && index == other.index; } } diff --git a/CryptoAnalysis/src/main/java/crypto/extractparameter/ExtractedValue.java b/CryptoAnalysis/src/main/java/crypto/extractparameter/ExtractedValue.java index 296a95029..9915dc2fd 100644 --- a/CryptoAnalysis/src/main/java/crypto/extractparameter/ExtractedValue.java +++ b/CryptoAnalysis/src/main/java/crypto/extractparameter/ExtractedValue.java @@ -3,68 +3,12 @@ import boomerang.scene.Statement; import boomerang.scene.Type; import boomerang.scene.Val; -import java.util.Arrays; import java.util.Collection; -public class ExtractedValue { - - private final Val val; - private final Statement initialStatement; - private final Collection types; - - public ExtractedValue(Val val, Statement initialStatement, Collection types) { - this.val = val; - this.initialStatement = initialStatement; - this.types = types; - } - - public Val getVal() { - return val; - } - - public Statement getInitialStatement() { - return initialStatement; - } - - public Collection getTypes() { - return types; - } +public record ExtractedValue(Val val, Statement initialStatement, Collection types) { @Override public String toString() { - return "Extracted Value: " + val + " with type " + types; - } - - @Override - public int hashCode() { - return Arrays.hashCode(new Object[] {val, initialStatement, types}); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (getClass() != obj.getClass()) return false; - - ExtractedValue other = (ExtractedValue) obj; - if (val == null) { - if (other.getVal() != null) return false; - } else if (!val.equals(other.getVal())) { - return false; - } - - if (initialStatement == null) { - if (other.getInitialStatement() != null) return false; - } else if (!initialStatement.equals(other.getInitialStatement())) { - return false; - } - - if (types == null) { - if (other.getTypes() != null) return false; - } else if (!types.equals(other.getTypes())) { - return false; - } - - return true; + return "Extracted Value: " + val + " with types " + types; } } diff --git a/CryptoAnalysis/src/main/java/crypto/reporting/ReportGenerator.java b/CryptoAnalysis/src/main/java/crypto/reporting/ReportGenerator.java index 646d591d8..3e0b71c9c 100644 --- a/CryptoAnalysis/src/main/java/crypto/reporting/ReportGenerator.java +++ b/CryptoAnalysis/src/main/java/crypto/reporting/ReportGenerator.java @@ -45,7 +45,6 @@ public static String generateReport( report.append("\t\tStatement: ").append(seed.getOrigin()).append("\n"); report.append("\t\tLine: ").append(seed.getOrigin().getStartLineNumber()).append("\n"); report.append("\t\tMethod: ").append(seed.getMethod()).append("\n"); - report.append("\t\tSHA-256: ").append(seed.getObjectId()).append("\n"); report.append("\t\tSecure: ").append(seed.isSecure()).append("\n"); } diff --git a/CryptoAnalysis/src/main/java/crypto/typestate/LabeledMatcherTransition.java b/CryptoAnalysis/src/main/java/crypto/typestate/LabeledMatcherTransition.java index 78cc40e39..9e0b8078f 100644 --- a/CryptoAnalysis/src/main/java/crypto/typestate/LabeledMatcherTransition.java +++ b/CryptoAnalysis/src/main/java/crypto/typestate/LabeledMatcherTransition.java @@ -3,8 +3,8 @@ import boomerang.scene.DeclaredMethod; import crypto.utils.MatcherUtils; import crysl.rule.CrySLMethod; -import java.util.Arrays; import java.util.Collection; +import java.util.Objects; import java.util.Optional; import typestate.finiteautomata.MatcherTransition; import typestate.finiteautomata.State; @@ -52,17 +52,14 @@ public String toString() { } @Override - public boolean equals(Object other) { - if (!super.equals(other)) { - return false; - } - - LabeledMatcherTransition matcherTransition = (LabeledMatcherTransition) other; - return this.methods.equals(matcherTransition.getMethods()); + public boolean equals(Object obj) { + return super.equals(obj) + && obj instanceof LabeledMatcherTransition other + && Objects.equals(methods, other.getMethods()); } @Override public int hashCode() { - return Arrays.hashCode(new Object[] {super.hashCode(), from(), to(), methods}); + return Objects.hash(super.hashCode(), from(), to(), methods); } } diff --git a/CryptoAnalysis/src/main/java/crypto/typestate/ReportingErrorStateNode.java b/CryptoAnalysis/src/main/java/crypto/typestate/ReportingErrorStateNode.java index 9b090ee23..e346a6217 100644 --- a/CryptoAnalysis/src/main/java/crypto/typestate/ReportingErrorStateNode.java +++ b/CryptoAnalysis/src/main/java/crypto/typestate/ReportingErrorStateNode.java @@ -4,17 +4,7 @@ import java.util.Collection; import typestate.finiteautomata.State; -public class ReportingErrorStateNode implements State { - - private final Collection expectedCalls; - - public ReportingErrorStateNode(Collection expectedCalls) { - this.expectedCalls = expectedCalls; - } - - public Collection getExpectedCalls() { - return expectedCalls; - } +public record ReportingErrorStateNode(Collection expectedCalls) implements State { @Override public boolean isErrorState() { @@ -35,14 +25,4 @@ public boolean isAccepting() { public String toString() { return "ERR"; } - - @Override - public int hashCode() { - return this.getClass().hashCode(); - } - - @Override - public boolean equals(Object obj) { - return obj instanceof ReportingErrorStateNode; - } } diff --git a/CryptoAnalysis/src/main/java/crypto/typestate/WrappedState.java b/CryptoAnalysis/src/main/java/crypto/typestate/WrappedState.java index 74740fe84..00c8cfe02 100644 --- a/CryptoAnalysis/src/main/java/crypto/typestate/WrappedState.java +++ b/CryptoAnalysis/src/main/java/crypto/typestate/WrappedState.java @@ -1,6 +1,7 @@ package crypto.typestate; import crysl.rule.StateNode; +import java.util.Objects; import typestate.finiteautomata.State; public class WrappedState implements State { @@ -46,22 +47,12 @@ public boolean isInitialState() { @Override public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((delegate == null) ? 0 : delegate.hashCode()); - return result; + return Objects.hash(delegate); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (getClass() != obj.getClass()) return false; - WrappedState other = (WrappedState) obj; - if (delegate == null) { - if (other.delegate != null) return false; - } else if (!delegate.equals(other.delegate)) return false; - return true; + return obj instanceof WrappedState other && Objects.equals(delegate, other.delegate()); } @Override diff --git a/CryptoAnalysis/src/test/java/test/assertions/ExtractedValueAssertion.java b/CryptoAnalysis/src/test/java/test/assertions/ExtractedValueAssertion.java index 2e3811705..2ac539a3a 100644 --- a/CryptoAnalysis/src/test/java/test/assertions/ExtractedValueAssertion.java +++ b/CryptoAnalysis/src/test/java/test/assertions/ExtractedValueAssertion.java @@ -19,13 +19,13 @@ public ExtractedValueAssertion(Statement stmt, int index) { public void computedValues(Collection collectedValues) { for (CallSiteWithExtractedValue callSite : collectedValues) { - Statement statement = callSite.getCallSiteWithParam().stmt(); + Statement statement = callSite.callSiteWithParam().statement(); - if (callSite.getExtractedValue().getVal().equals(Val.zero())) { + if (callSite.extractedValue().val().equals(Val.zero())) { continue; } - if (statement.equals(stmt) && callSite.getCallSiteWithParam().getIndex() == index) { + if (statement.equals(stmt) && callSite.callSiteWithParam().index() == index) { satisfied = true; } } From df9f71827db3aca2fae301e8c5f7bb5cb406a0a3 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Mon, 16 Dec 2024 12:07:33 +0100 Subject: [PATCH 15/32] Fix connections for subsequent errors --- .../crypto/analysis/AbstractPredicate.java | 37 +++ .../AnalysisSeedWithEnsuredPredicate.java | 2 +- .../AnalysisSeedWithSpecification.java | 222 +++++++++--------- .../analysis/EnsuredCrySLPredicate.java | 29 +-- .../java/crypto/analysis/HiddenPredicate.java | 167 ++++++------- .../java/crypto/analysis/IAnalysisSeed.java | 7 +- .../crypto/analysis/PredicateHandler.java | 77 +++--- .../errors/AbstractConstraintsError.java | 4 +- .../crypto/analysis/errors/AbstractError.java | 24 +- .../errors/AbstractRequiresError.java | 23 -- .../errors/PredicateConstraintError.java | 59 +++++ .../errors/PredicateContradictionError.java | 2 +- .../errors/RequiredPredicateError.java | 101 +++----- .../crypto/constraints/ValueConstraint.java | 5 +- .../crypto/listener/AnalysisReporter.java | 46 ++-- .../crypto/listener/IResultsListener.java | 3 +- .../test/UsagePatternResultsListener.java | 3 +- .../HasEnsuredPredicateAssertion.java | 4 +- .../HasGeneratedPredicateAssertion.java | 4 +- .../HasNotGeneratedPredicateAssertion.java | 4 +- .../NotHasEnsuredPredicateAssertion.java | 4 +- .../scanner/setup/AbstractHeadlessTest.java | 6 +- .../targets/BragaCryptoGoodUsesTest.java | 6 +- .../targets/BragaCryptoMisusesTest.java | 12 +- 24 files changed, 432 insertions(+), 419 deletions(-) create mode 100644 CryptoAnalysis/src/main/java/crypto/analysis/AbstractPredicate.java delete mode 100644 CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiresError.java create mode 100644 CryptoAnalysis/src/main/java/crypto/analysis/errors/PredicateConstraintError.java diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/AbstractPredicate.java b/CryptoAnalysis/src/main/java/crypto/analysis/AbstractPredicate.java new file mode 100644 index 000000000..47ae33dc8 --- /dev/null +++ b/CryptoAnalysis/src/main/java/crypto/analysis/AbstractPredicate.java @@ -0,0 +1,37 @@ +package crypto.analysis; + +import crypto.extractparameter.CallSiteWithExtractedValue; +import crysl.rule.CrySLPredicate; +import java.util.Collection; +import java.util.Objects; + +public abstract class AbstractPredicate { + + private final CrySLPredicate predicate; + private final Collection parametersToValues; + + public AbstractPredicate( + CrySLPredicate predicate, Collection parametersToValues) { + this.predicate = predicate; + this.parametersToValues = parametersToValues; + } + + public CrySLPredicate getPredicate() { + return predicate; + } + + public Collection getParametersToValues() { + return parametersToValues; + } + + @Override + public int hashCode() { + return Objects.hash(predicate); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof AbstractPredicate other + && Objects.equals(predicate, other.getPredicate()); + } +} diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithEnsuredPredicate.java b/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithEnsuredPredicate.java index 21ae0df22..4f5f8684d 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithEnsuredPredicate.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithEnsuredPredicate.java @@ -105,7 +105,7 @@ public void expectPredicate( } } - public void addEnsuredPredicate(EnsuredCrySLPredicate predicate) { + public void addEnsuredPredicate(AbstractPredicate predicate) { for (Statement statement : expectedPredicates.keySet()) { Collection predicateOnSeeds = expectedPredicates.get(statement); diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java b/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java index 56d4ae1d6..1db361d42 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java @@ -14,7 +14,6 @@ import crypto.analysis.errors.AbstractError; import crypto.analysis.errors.ForbiddenMethodError; import crypto.analysis.errors.IncompleteOperationError; -import crypto.analysis.errors.RequiredPredicateError; import crypto.analysis.errors.TypestateError; import crypto.constraints.ConstraintSolver; import crypto.constraints.EvaluableConstraint; @@ -39,6 +38,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -62,7 +62,7 @@ public class AnalysisSeedWithSpecification extends IAnalysisSeed { HashMultimap.create(); private final Multimap> hiddenPredicates = HashMultimap.create(); - private final Collection indirectlyEnsuredPredicates = new HashSet<>(); + private final Collection indirectlyEnsuredPredicates = new HashSet<>(); public AnalysisSeedWithSpecification( CryptoScanner scanner, @@ -122,6 +122,7 @@ private void evaluateForbiddenMethods() { ForbiddenMethodError error = new ForbiddenMethodError( this, statement, specification, declaredMethod, alternatives); + this.addError(error); scanner.getAnalysisReporter().reportError(this, error); } } @@ -356,7 +357,8 @@ private void initializeDependantSeedsFromRequiredPredicates(Collection requiredSeeds = computeRequiredSeeds(statement, base, seeds); for (IAnalysisSeed seed : requiredSeeds) { - seed.expectPredicate(statement, predicate, this, paramIndex); + seed.expectPredicate( + statement, predicate.toNormalCrySLPredicate(), this, paramIndex); if (seed instanceof AnalysisSeedWithSpecification) { ((AnalysisSeedWithSpecification) seed).addRequiringSeed(this); @@ -368,7 +370,8 @@ private void initializeDependantSeedsFromRequiredPredicates(Collection requiredSeeds = computeRequiredSeeds(statement, param, seeds); for (IAnalysisSeed seed : requiredSeeds) { - seed.expectPredicate(statement, predicate, this, paramIndex); + seed.expectPredicate( + statement, predicate.toNormalCrySLPredicate(), this, paramIndex); if (seed instanceof AnalysisSeedWithSpecification) { ((AnalysisSeedWithSpecification) seed).addRequiringSeed(this); @@ -439,7 +442,7 @@ private void initializeDependantSeedsFromEnsuringPredicates(Collection methods = @@ -456,7 +459,8 @@ private void initializeDependantSeedsFromEnsuringPredicates(Collection dependentAssignSeeds = computeGeneratedAssignSeeds(statement, allocVal, seeds); for (IAnalysisSeed seed : dependentAssignSeeds) { - this.expectPredicate(statement, predicate, seed, -1); + this.expectPredicate( + statement, predicate.toNormalCrySLPredicate(), seed, -1); } } @@ -469,7 +473,8 @@ private void initializeDependantSeedsFromEnsuringPredicates(Collection dependantParamSeeds = computeGeneratedParameterSeeds(entry.getKey(), paramVal, seeds); for (IAnalysisSeed seed : dependantParamSeeds) { - this.expectPredicate(statement, predicate, seed, i); + this.expectPredicate( + statement, predicate.toNormalCrySLPredicate(), seed, i); } } } @@ -546,86 +551,87 @@ public void ensurePredicates() { Collection expectedPredStatements = expectedPredicates.keySet(); Collection predsToBeEnsured = new HashSet<>(specification.getPredicates()); - for (EnsuredCrySLPredicate predicate : indirectlyEnsuredPredicates) { + for (AbstractPredicate predicate : indirectlyEnsuredPredicates) { predsToBeEnsured.add(predicate.getPredicate().toNormalCrySLPredicate()); } for (CrySLPredicate predToBeEnsured : predsToBeEnsured) { + Collection violations = new HashSet<>(); - for (Statement statement : expectedPredStatements) { - boolean isPredicateGeneratingStateAvailable = false; + if (errorCollection.stream().anyMatch(e -> e instanceof ForbiddenMethodError)) { + violations.add(HiddenPredicate.Violations.CallToForbiddenMethod); + } + + if (!satisfiesConstraintSystem) { + violations.add(HiddenPredicate.Violations.ConstraintsAreNotSatisfied); + } + + if (predToBeEnsured.getConstraint().isPresent() + && isPredConditionViolated(predToBeEnsured)) { + violations.add(HiddenPredicate.Violations.ConditionIsNotSatisfied); + } + for (Statement statement : expectedPredStatements) { if (!expectedPredicatesAtStatement(statement) .contains(predToBeEnsured.toNormalCrySLPredicate())) { continue; } + /* Check for all states whether an accepting state is reached: + * 1) All states are accepting -> Predicate is generated + * 2) No state is accepting -> Predicate is definitely not generated + * 3) There are generating and non-generating states -> At least one + * dataflow path leads to a non-generating state s.t. the predicate + * is not generated (over approximation) + */ Collection states = getStatesAtStatement(statement); - - for (State state : states) { - // Check, whether the predicate should be generated in state - if (!isPredicateGeneratingState(predToBeEnsured, state)) { - continue; - } - - // Check, whether the predicate is not negated in state - if (isPredicateNegatingState(predToBeEnsured, state)) { - continue; - } - - isPredicateGeneratingStateAvailable = true; - EnsuredCrySLPredicate ensPred; - if (!satisfiesConstraintSystem && predToBeEnsured.getConstraint().isEmpty()) { - // predicate has no condition, but the constraint system is not satisfied - ensPred = - new HiddenPredicate( - predToBeEnsured, - constraintSolver.getCollectedValues(), - this, - HiddenPredicate.HiddenPredicateType - .ConstraintsAreNotSatisfied); - } else if (predToBeEnsured.getConstraint().isPresent() - && isPredConditionViolated(predToBeEnsured)) { - // predicate has condition, but condition is not satisfied - ensPred = - new HiddenPredicate( - predToBeEnsured, - constraintSolver.getCollectedValues(), - this, - HiddenPredicate.HiddenPredicateType - .ConditionIsNotSatisfied); - } else { - // constraints are satisfied and predicate has no condition or the condition - // is satisfied - ensPred = - new EnsuredCrySLPredicate( - predToBeEnsured, constraintSolver.getCollectedValues()); - } - - ensurePredicateAtStatement(ensPred, statement); + boolean allStatesNonGenerating = + states.stream() + .noneMatch(s -> doesStateGeneratePredicate(s, predToBeEnsured)); + boolean someStatesNonGenerating = + states.stream() + .anyMatch(s -> !doesStateGeneratePredicate(s, predToBeEnsured)); + + Collection allViolations = new HashSet<>(violations); + if (allStatesNonGenerating) { + allViolations.add(HiddenPredicate.Violations.GeneratingStateIsNeverReached); + } else if (someStatesNonGenerating) { + /* TODO + * Due to a bug, IDEal returns the states [0,1] whenever there is a + * single call to a method, e.g. Object o = new Object(); o.m();. After + * the call to m1(), o is always in state 0 and 1, although it should only be 1 + */ + // allViolations.add(HiddenPredicate.Violations.GeneratingStateMayNotBeReached); } - if (!isPredicateGeneratingStateAvailable) { - /* The predicate is not ensured in any state. However, we propagate a hidden predicate - * for all typestate changing statements because the predicate could have been ensured - * if a generating state had been reached - */ - HiddenPredicate hiddenPredicate = + AbstractPredicate generatedPred; + if (!allViolations.isEmpty()) { + generatedPred = new HiddenPredicate( - predToBeEnsured, + predToBeEnsured.toNormalCrySLPredicate(), constraintSolver.getCollectedValues(), this, - HiddenPredicate.HiddenPredicateType - .GeneratingStateIsNeverReached); - ensurePredicateAtStatement(hiddenPredicate, statement); + allViolations); + } else { + generatedPred = + new EnsuredCrySLPredicate( + predToBeEnsured.toNormalCrySLPredicate(), + constraintSolver.getCollectedValues()); } + + ensurePredicateAtStatement(generatedPred, statement); } } scanner.getAnalysisReporter().ensuredPredicates(this, ensuredPredicates); } - private void ensurePredicateAtStatement(EnsuredCrySLPredicate ensPred, Statement statement) { + private boolean doesStateGeneratePredicate(State state, CrySLPredicate predicate) { + return isPredicateGeneratingState(predicate, state) + && !isPredicateNegatingState(predicate, state); + } + + private void ensurePredicateAtStatement(AbstractPredicate ensPred, Statement statement) { if (hasThisParameter(ensPred.getPredicate())) { this.addEnsuredPredicate(ensPred, statement, -1); scanner.getAnalysisReporter().onGeneratedPredicate(this, ensPred, this, statement); @@ -657,7 +663,7 @@ private void ensurePredicateAtStatement(EnsuredCrySLPredicate ensPred, Statement } private void addEnsuredPredicateFromOtherRule( - EnsuredCrySLPredicate pred, Statement statement, int paramIndex) { + AbstractPredicate pred, Statement statement, int paramIndex) { addEnsuredPredicate(pred, statement, paramIndex); indirectlyEnsuredPredicates.add(pred); } @@ -695,14 +701,14 @@ private Collection getStatesAtStatement(Statement statement) { } public void addEnsuredPredicate( - EnsuredCrySLPredicate ensPred, Statement statement, int paramIndex) { + AbstractPredicate ensPred, Statement statement, int paramIndex) { if (ensPred instanceof HiddenPredicate hiddenPredicate) { Map.Entry predAtIndex = new AbstractMap.SimpleEntry<>(hiddenPredicate, paramIndex); hiddenPredicates.put(statement, predAtIndex); - } else { + } else if (ensPred instanceof EnsuredCrySLPredicate ensuredCrySLPredicate) { Map.Entry predAtIndex = - new AbstractMap.SimpleEntry<>(ensPred, paramIndex); + new AbstractMap.SimpleEntry<>(ensuredCrySLPredicate, paramIndex); ensuredPredicates.put(statement, predAtIndex); } } @@ -806,6 +812,7 @@ private void checkInternalConstraints() { Collection violatedConstraints = constraintSolver.evaluateConstraints(); for (AbstractError violatedConstraint : violatedConstraints) { + this.addError(violatedConstraint); scanner.getAnalysisReporter().reportError(this, violatedConstraint); } @@ -982,37 +989,27 @@ private boolean isPredConditionViolated(CrySLPredicate pred) { .orElse(false); } - public Collection retrieveErrorsForPredCondition(CrySLPredicate pred) { - // Check, whether the predicate has a condition - if (pred.getConstraint().isEmpty()) { - return Collections.emptyList(); - } - - // TODO the condition should be reported itself? - ISLConstraint condition = pred.getConstraint().get(); - return Collections.emptyList(); - } - - private boolean doPredsMatch(CrySLPredicate pred, EnsuredCrySLPredicate ensPred) { + private boolean doPredsMatch(CrySLPredicate pred, AbstractPredicate ensPred) { boolean requiredPredicatesExist = true; for (int i = 0; i < pred.getParameters().size(); i++) { String var = pred.getParameters().get(i).getName(); if (isOfNonTrackableType(var)) { continue; - } else if (pred.getInvolvedVarNames().contains(var)) { + } + if (pred.getInvolvedVarNames().contains(var)) { final String parameterI = ensPred.getPredicate().getParameters().get(i).getName(); Collection actVals = Collections.emptySet(); Collection expVals = Collections.emptySet(); - for (CallSiteWithExtractedValue cswpi : ensPred.getParametersToValues()) { - if (cswpi.callSiteWithParam().varName().equals(parameterI)) { - actVals = retrieveValueFromUnit(cswpi); + for (CallSiteWithExtractedValue callSite : ensPred.getParametersToValues()) { + if (callSite.callSiteWithParam().varName().equals(parameterI)) { + actVals = retrieveValueFromUnit(callSite); } } - for (CallSiteWithExtractedValue cswpi : constraintSolver.getCollectedValues()) { - if (cswpi.callSiteWithParam().varName().equals(var)) { - expVals = retrieveValueFromUnit(cswpi); + for (CallSiteWithExtractedValue callSite : constraintSolver.getCollectedValues()) { + if (callSite.callSiteWithParam().varName().equals(var)) { + expVals = retrieveValueFromUnit(callSite); } } @@ -1041,21 +1038,42 @@ private boolean doPredsMatch(CrySLPredicate pred, EnsuredCrySLPredicate ensPred) return requiredPredicatesExist; } - public void addHiddenPredicatesToError(RequiredPredicateError reqPredError) { - for (CrySLPredicate pred : reqPredError.getContradictedPredicates()) { - Collection hiddenPredicatesEnsuringReqPred = new HashSet<>(); + public Collection extractHiddenPredicates(RequiredCrySLPredicate predicate) { + return extractHiddenPredicates( + predicate.getLocation(), List.of(predicate.getPred()), predicate.getParamIndex()); + } + + public Collection extractHiddenPredicates(AlternativeReqPredicate predicate) { + return extractHiddenPredicates( + predicate.getLocation(), predicate.getAlternatives(), predicate.getParamIndex()); + } - for (Map.Entry entry : hiddenPredicates.values()) { + private Collection extractHiddenPredicates( + Statement statement, Collection predicates, int index) { + Collection result = new HashSet<>(); + + if (!hiddenPredicates.containsKey(statement)) { + return result; + } + + Collection> hiddenPreds = + hiddenPredicates.get(statement); + for (Map.Entry entry : hiddenPreds) { + if (entry.getValue() != index) { + continue; + } + + for (CrySLPredicate pred : predicates) { HiddenPredicate hiddenPredicate = entry.getKey(); if (hiddenPredicate.getPredicate().equals(pred) && doPredsMatch(pred, hiddenPredicate)) { - hiddenPredicatesEnsuringReqPred.add(hiddenPredicate); + result.add(hiddenPredicate); } } - - reqPredError.addHiddenPredicates(hiddenPredicatesEnsuringReqPred); } + + return result; } private Collection retrieveValueFromUnit(CallSiteWithExtractedValue callSite) { @@ -1065,23 +1083,17 @@ private Collection retrieveValueFromUnit(CallSiteWithExtractedValue call if (statement.isAssign()) { Val rightSide = statement.getRightOp(); - if (rightSide.isConstant()) { - values.add(retrieveConstantFromValue(rightSide)); + if (rightSide.isIntConstant()) { + values.add(String.valueOf(rightSide.getIntValue())); + } else if (rightSide.isLongConstant()) { + values.add(String.valueOf(rightSide.getLongValue())); + } else if (rightSide.isStringConstant()) { + values.add(rightSide.getStringValue()); } } return values; } - private String retrieveConstantFromValue(Val val) { - if (val.isStringConstant()) { - return val.getStringValue(); - } else if (val.isIntConstant()) { - return String.valueOf(val.getIntValue()); - } else { - return ""; - } - } - private static final Collection trackedTypes = Arrays.asList("java.lang.String", "int", "java.lang.Integer"); diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/EnsuredCrySLPredicate.java b/CryptoAnalysis/src/main/java/crypto/analysis/EnsuredCrySLPredicate.java index 974a5b501..f47c82367 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/EnsuredCrySLPredicate.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/EnsuredCrySLPredicate.java @@ -5,36 +5,25 @@ import java.util.Collection; import java.util.Objects; -public class EnsuredCrySLPredicate { - - private final CrySLPredicate predicate; - private final Collection parametersToValues; +public class EnsuredCrySLPredicate extends AbstractPredicate { public EnsuredCrySLPredicate( CrySLPredicate predicate, Collection parametersToValues) { - this.predicate = predicate; - this.parametersToValues = parametersToValues; - } - - public CrySLPredicate getPredicate() { - return predicate; - } - - public Collection getParametersToValues() { - return parametersToValues; - } - - public String toString() { - return "Proved " + predicate.getPredName(); + super(predicate, parametersToValues); } @Override public int hashCode() { - return Objects.hash(predicate); + return Objects.hash(super.hashCode()); } @Override public boolean equals(Object obj) { - return obj instanceof EnsuredCrySLPredicate other && predicate.equals(other.predicate); + return super.equals(obj) && obj instanceof EnsuredCrySLPredicate; + } + + @Override + public String toString() { + return "Ensured: " + getPredicate().getPredName(); } } diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/HiddenPredicate.java b/CryptoAnalysis/src/main/java/crypto/analysis/HiddenPredicate.java index 6ecc4cb97..8dd0cf2bb 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/HiddenPredicate.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/HiddenPredicate.java @@ -1,122 +1,101 @@ package crypto.analysis; -import com.google.common.collect.Lists; +import crypto.analysis.errors.AbstractConstraintsError; import crypto.analysis.errors.AbstractError; -import crypto.analysis.errors.ConstraintError; -import crypto.analysis.errors.HardCodedError; -import crypto.analysis.errors.ImpreciseValueExtractionError; -import crypto.analysis.errors.IncompleteOperationError; -import crypto.analysis.errors.InstanceOfError; -import crypto.analysis.errors.NeverTypeOfError; -import crypto.analysis.errors.RequiredPredicateError; -import crypto.analysis.errors.TypestateError; +import crypto.analysis.errors.AbstractOrderError; +import crypto.analysis.errors.ForbiddenMethodError; +import crypto.analysis.errors.PredicateConstraintError; import crypto.extractparameter.CallSiteWithExtractedValue; import crysl.rule.CrySLPredicate; import java.util.Collection; -import java.util.stream.Collectors; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; -public class HiddenPredicate extends EnsuredCrySLPredicate { +public class HiddenPredicate extends AbstractPredicate { private final AnalysisSeedWithSpecification generatingSeed; - private final HiddenPredicateType type; + private final Collection violations; + + public enum Violations { + CallToForbiddenMethod, + ConstraintsAreNotSatisfied, + ConditionIsNotSatisfied, + GeneratingStateMayNotBeReached, + GeneratingStateIsNeverReached + } public HiddenPredicate( CrySLPredicate predicate, Collection parametersToValues, AnalysisSeedWithSpecification generatingSeed, - HiddenPredicateType type) { + Collection violations) { super(predicate, parametersToValues); + this.generatingSeed = generatingSeed; - this.type = type; + this.violations = Set.copyOf(violations); } public AnalysisSeedWithSpecification getGeneratingSeed() { return generatingSeed; } - public enum HiddenPredicateType { - GeneratingStateIsNeverReached, - ConstraintsAreNotSatisfied, - ConditionIsNotSatisfied + public Collection getViolations() { + return violations; } - public HiddenPredicateType getType() { - return type; - } - - /** - * Node: Errors are only in complete count at the end of the analysis. - * - * @return errors list of all preceding errors - */ public Collection getPrecedingErrors() { - Collection results = Lists.newArrayList(); - Collection allErrors = generatingSeed.getErrors(); - switch (type) { - case GeneratingStateIsNeverReached: - Collection typestateErrors = - allErrors.stream() - .filter( - e -> - (e instanceof IncompleteOperationError - || e instanceof TypestateError)) - .collect(Collectors.toList()); - if (typestateErrors.isEmpty()) { - // Seed object has no typestate errors that might be responsible for this hidden - // predicate - // TODO: report new info error type to report, - // that the seeds object could potentially ensure the missing predicate which - // might cause further subsequent errors - // but therefore requires a call to the predicate generating statement - } - - // TODO: check whether the generating state is not reached due to a typestate error - return allErrors; - - case ConstraintsAreNotSatisfied: - // Generating state was reached but constraints are not satisfied. - // Thus, return all constraints & required predicate errors. - return allErrors.stream() - .filter( - e -> - (e instanceof RequiredPredicateError - || e instanceof ConstraintError - || e instanceof HardCodedError - || e instanceof ImpreciseValueExtractionError - || e instanceof InstanceOfError - || e instanceof NeverTypeOfError)) - .collect(Collectors.toList()); - case ConditionIsNotSatisfied: - // Generating state was reached but the predicates condition is not satisfied. - // Thus, return all errors that causes the condition to be not satisfied - Collection precedingErrors = - Lists.newArrayList( - generatingSeed.retrieveErrorsForPredCondition(this.getPredicate())); - // This method is called from a RequiredPredicateError that wants to retrieve its - // preceding errors. - // In this case, preceding errors are not reported yet because the predicate - // condition wasn't required to be satisfied. - // Since the hidden predicate is required to be an ensured predicate, we can assume - // the condition required to be satisfied. - // Thus, we report all errors that causes the condition to be not satisfied. - // precedingErrors.forEach(e -> - // this.generatingSeed.scanner.getAnalysisListener().reportError(generatingSeed, - // e)); - precedingErrors.forEach( - e -> - this.generatingSeed - .scanner - .getAnalysisReporter() - .reportError(generatingSeed, e)); - // Further, preceding errors can be of type RequiredPredicateError. - // Thus, we have to recursively map preceding errors for the newly reported errors. - for (AbstractError e : precedingErrors) { - if (e instanceof RequiredPredicateError) { - ((RequiredPredicateError) e).mapPrecedingErrors(); - } - } - return precedingErrors; + Collection result = new HashSet<>(); + + if (violations.contains(Violations.CallToForbiddenMethod)) { + Collection forbiddenMethodErrors = + generatingSeed.getErrors().stream() + .filter(e -> e instanceof ForbiddenMethodError) + .toList(); + result.addAll(forbiddenMethodErrors); } - return results; + + if (violations.contains(Violations.ConstraintsAreNotSatisfied)) { + Collection constraintErrors = + generatingSeed.getErrors().stream() + .filter(e -> e instanceof AbstractConstraintsError) + .toList(); + result.addAll(constraintErrors); + } + + if (violations.contains(Violations.ConditionIsNotSatisfied)) { + PredicateConstraintError error = + new PredicateConstraintError(generatingSeed, getPredicate()); + result.add(error); + } + + if (violations.contains(Violations.GeneratingStateMayNotBeReached) + || violations.contains(Violations.GeneratingStateIsNeverReached)) { + Collection orderError = + generatingSeed.getErrors().stream() + .filter(e -> e instanceof AbstractOrderError) + .toList(); + result.addAll(orderError); + } + + return result; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), generatingSeed, violations); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj) + && obj instanceof HiddenPredicate other + && Objects.equals(generatingSeed, other.getGeneratingSeed()) + && Objects.equals(violations, other.getViolations()); + } + + @Override + public String toString() { + return "Hidden: " + getPredicate().getPredName(); } } diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/IAnalysisSeed.java b/CryptoAnalysis/src/main/java/crypto/analysis/IAnalysisSeed.java index e6750e823..bab7222b9 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/IAnalysisSeed.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/IAnalysisSeed.java @@ -27,7 +27,6 @@ protected record ExpectedPredicateOnSeed( private final Statement origin; private final Val fact; - private boolean secure = true; public IAnalysisSeed( CryptoScanner scanner, @@ -80,11 +79,7 @@ public Type getType() { } public boolean isSecure() { - return secure; - } - - public void setSecure(boolean secure) { - this.secure = secure; + return errorCollection.isEmpty(); } public ForwardBoomerangResults getAnalysisResults() { diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/PredicateHandler.java b/CryptoAnalysis/src/main/java/crypto/analysis/PredicateHandler.java index 98068b080..fc4ba696d 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/PredicateHandler.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/PredicateHandler.java @@ -1,5 +1,6 @@ package crypto.analysis; +import crypto.analysis.errors.AbstractError; import crypto.analysis.errors.PredicateContradictionError; import crypto.analysis.errors.RequiredPredicateError; import crysl.rule.ISLConstraint; @@ -23,13 +24,15 @@ public PredicateHandler(CryptoScanner cryptoScanner) { public void checkPredicates() { runPredicateMechanism(); - collectMissingRequiredPredicates(); collectContradictingPredicates(); + collectMissingRequiredPredicates(); reportRequiredPredicateErrors(); + + // Connections are only available once all errors have been reported + connectSubsequentErrors(); } private void runPredicateMechanism() { - for (AnalysisSeedWithSpecification seed : cryptoScanner.getAnalysisSeedsWithSpec()) { seed.computeExpectedPredicates(cryptoScanner.getDiscoveredSeeds()); } @@ -42,58 +45,68 @@ private void runPredicateMechanism() { } } + private void collectContradictingPredicates() { + for (AnalysisSeedWithSpecification seed : cryptoScanner.getAnalysisSeedsWithSpec()) { + Collection contradictedPredicates = + seed.computeContradictedPredicates(); + + for (RequiredCrySLPredicate pred : contradictedPredicates) { + PredicateContradictionError error = + new PredicateContradictionError( + seed, pred.getLocation(), seed.getSpecification(), pred.getPred()); + seed.addError(error); + cryptoScanner.getAnalysisReporter().reportError(seed, error); + } + } + } + private void collectMissingRequiredPredicates() { for (AnalysisSeedWithSpecification seed : cryptoScanner.getAnalysisSeedsWithSpec()) { requiredPredicateErrors.put(seed, new ArrayList<>()); Collection missingPredicates = seed.computeMissingPredicates(); for (ISLConstraint pred : missingPredicates) { - if (pred instanceof RequiredCrySLPredicate) { - RequiredCrySLPredicate reqPred = (RequiredCrySLPredicate) pred; + if (pred instanceof RequiredCrySLPredicate reqPred) { + Collection hiddenPreds = seed.extractHiddenPredicates(reqPred); - RequiredPredicateError reqPredError = new RequiredPredicateError(seed, reqPred); - addRequiredPredicateErrorOnSeed(reqPredError, seed); - } else if (pred instanceof AlternativeReqPredicate) { - AlternativeReqPredicate altReqPred = (AlternativeReqPredicate) pred; + RequiredPredicateError reqPredError = + new RequiredPredicateError(seed, reqPred, hiddenPreds); + requiredPredicateErrors.get(seed).add(reqPredError); + } else if (pred instanceof AlternativeReqPredicate altReqPred) { + Collection hiddenPreds = + seed.extractHiddenPredicates(altReqPred); RequiredPredicateError reqPredError = - new RequiredPredicateError(seed, altReqPred); - addRequiredPredicateErrorOnSeed(reqPredError, seed); + new RequiredPredicateError(seed, altReqPred, hiddenPreds); + requiredPredicateErrors.get(seed).add(reqPredError); } } } } - private void addRequiredPredicateErrorOnSeed( - RequiredPredicateError reqPredError, AnalysisSeedWithSpecification seed) { - seed.addHiddenPredicatesToError(reqPredError); - seed.addError(reqPredError); - requiredPredicateErrors.get(seed).add(reqPredError); - } - private void reportRequiredPredicateErrors() { - for (Map.Entry> entry : - requiredPredicateErrors.entrySet()) { - AnalysisSeedWithSpecification seed = entry.getKey(); + for (AnalysisSeedWithSpecification seed : requiredPredicateErrors.keySet()) { + Collection errors = requiredPredicateErrors.get(seed); - for (RequiredPredicateError reqPredError : entry.getValue()) { - reqPredError.mapPrecedingErrors(); + for (RequiredPredicateError reqPredError : errors) { + seed.addError(reqPredError); cryptoScanner.getAnalysisReporter().reportError(seed, reqPredError); } } } - private void collectContradictingPredicates() { - for (AnalysisSeedWithSpecification seed : cryptoScanner.getAnalysisSeedsWithSpec()) { - Collection contradictedPredicates = - seed.computeContradictedPredicates(); + private void connectSubsequentErrors() { + for (AnalysisSeedWithSpecification seed : requiredPredicateErrors.keySet()) { + Collection errors = requiredPredicateErrors.get(seed); - for (RequiredCrySLPredicate pred : contradictedPredicates) { - PredicateContradictionError error = - new PredicateContradictionError( - seed, pred.getLocation(), seed.getSpecification(), pred.getPred()); - seed.addError(error); - cryptoScanner.getAnalysisReporter().reportError(seed, error); + for (RequiredPredicateError error : errors) { + for (HiddenPredicate hiddenPredicate : error.getHiddenPredicates()) { + Collection precedingErrors = + hiddenPredicate.getPrecedingErrors(); + + precedingErrors.forEach(error::addPrecedingError); + precedingErrors.forEach(e -> e.addSubsequentError(error)); + } } } } diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractConstraintsError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractConstraintsError.java index d15e2907d..b52f40f7a 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractConstraintsError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractConstraintsError.java @@ -4,7 +4,9 @@ import crypto.analysis.IAnalysisSeed; import crysl.rule.CrySLRule; -/** Super class for all errors that violate a constraint from the CONSTRAINTS section */ +/** + * Super class for all errors that violate a constraint from the CONSTRAINTS and REQUIRES section + */ public abstract class AbstractConstraintsError extends AbstractError { public AbstractConstraintsError(IAnalysisSeed seed, Statement errorStmt, CrySLRule rule) { diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractError.java index c9171f630..f82a7cd92 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractError.java @@ -16,16 +16,16 @@ public abstract class AbstractError { private final Statement errorStmt; private final CrySLRule rule; - private final Collection causedByErrors; // preceding - private final Collection willCauseErrors; // subsequent + private final Collection precedingErrors; // preceding + private final Collection subsequentErrors; // subsequent public AbstractError(IAnalysisSeed seed, Statement errorStmt, CrySLRule rule) { this.seed = seed; this.errorStmt = errorStmt; this.rule = rule; - this.causedByErrors = new HashSet<>(); - this.willCauseErrors = new HashSet<>(); + this.precedingErrors = new HashSet<>(); + this.subsequentErrors = new HashSet<>(); } public abstract String toErrorMarkerString(); @@ -50,24 +50,28 @@ public int getLineNumber() { return errorStmt.getStartLineNumber(); } - public void addCausingError(AbstractError parent) { - causedByErrors.add(parent); + public void addPrecedingError(AbstractError error) { + precedingErrors.add(error); } public void addCausingError(Collection parents) { - causedByErrors.addAll(parents); + precedingErrors.addAll(parents); } public void addSubsequentError(AbstractError subsequentError) { - willCauseErrors.add(subsequentError); + subsequentErrors.add(subsequentError); + } + + public Collection getPrecedingErrors() { + return precedingErrors; } public Collection getSubsequentErrors() { - return this.willCauseErrors; + return subsequentErrors; } public Collection getRootErrors() { - return this.causedByErrors; + return this.precedingErrors; } public String toString() { diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiresError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiresError.java deleted file mode 100644 index 353bb21bf..000000000 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiresError.java +++ /dev/null @@ -1,23 +0,0 @@ -package crypto.analysis.errors; - -import boomerang.scene.Statement; -import crypto.analysis.IAnalysisSeed; -import crysl.rule.CrySLRule; - -/** Super class for all errors that violate a constraint from the REQUIRES section */ -public abstract class AbstractRequiresError extends AbstractError { - - public AbstractRequiresError(IAnalysisSeed seed, Statement errorStmt, CrySLRule rule) { - super(seed, errorStmt, rule); - } - - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public boolean equals(Object obj) { - return super.equals(obj); - } -} diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/PredicateConstraintError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/PredicateConstraintError.java new file mode 100644 index 000000000..764464e4b --- /dev/null +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/PredicateConstraintError.java @@ -0,0 +1,59 @@ +package crypto.analysis.errors; + +import crypto.analysis.AnalysisSeedWithSpecification; +import crysl.rule.CrySLPredicate; +import java.util.Objects; + +/** + * This class represents an internal error if the constraint of a predicate to be ensured is + * violated and is only used to propagate HiddenPredicates. For example, we have the following + * ENSURES block: + * + *
{@code
+ * ENSURES
+ *    algorithm in {"AES"} => generatedKey[...]
+ * }
+ * + * If the algorithm is not "AES", the predicate "generatedKey" is not ensured. Instead, the analysis + * propagates a HiddenPredicate with the cause that the constraint is not satisfied to have a valid + * reason. This class then simply indicates that the predicate's constraint is not satisfied. This + * error is/should be not reported. + */ +public class PredicateConstraintError extends AbstractError { + + private final CrySLPredicate predicate; + + public PredicateConstraintError(AnalysisSeedWithSpecification seed, CrySLPredicate predicate) { + super(seed, seed.getOrigin(), seed.getSpecification()); + + this.predicate = predicate; + } + + public CrySLPredicate getPredicate() { + return predicate; + } + + @Override + public String toErrorMarkerString() { + return "Cannot ensure predicate " + + predicate.getPredName() + + " because its constraint is not satisfied"; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), predicate); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj) + && obj instanceof PredicateConstraintError other + && Objects.equals(predicate, other.getPredicate()); + } + + @Override + public String toString() { + return "PredicateConstraintError: " + toErrorMarkerString(); + } +} diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/PredicateContradictionError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/PredicateContradictionError.java index 712dc2c9b..fa4375904 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/PredicateContradictionError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/PredicateContradictionError.java @@ -6,7 +6,7 @@ import crysl.rule.CrySLRule; import java.util.Objects; -public class PredicateContradictionError extends AbstractRequiresError { +public class PredicateContradictionError extends AbstractConstraintsError { private final CrySLPredicate contradictedPredicate; diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java index 941b44d02..84418eeda 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java @@ -5,44 +5,44 @@ import crypto.analysis.HiddenPredicate; import crypto.analysis.RequiredCrySLPredicate; import crysl.rule.CrySLPredicate; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; /** * Creates {@link RequiredPredicateError} for all Required Predicate error generates * RequiredPredicateError */ -public class RequiredPredicateError extends AbstractRequiresError { +public class RequiredPredicateError extends AbstractConstraintsError { - private final Collection hiddenPredicates; private final Collection contradictedPredicates; + private final Collection hiddenPredicates; private final int paramIndex; public RequiredPredicateError( - AnalysisSeedWithSpecification seed, RequiredCrySLPredicate violatedPred) { + AnalysisSeedWithSpecification seed, + RequiredCrySLPredicate violatedPred, + Collection hiddenPredicates) { super(seed, violatedPred.getLocation(), seed.getSpecification()); - this.hiddenPredicates = new HashSet<>(); - this.contradictedPredicates = Collections.singletonList(violatedPred.getPred()); + this.hiddenPredicates = Set.copyOf(hiddenPredicates); + this.contradictedPredicates = List.of(violatedPred.getPred()); this.paramIndex = violatedPred.getParamIndex(); } public RequiredPredicateError( - AnalysisSeedWithSpecification seed, AlternativeReqPredicate violatedPred) { + AnalysisSeedWithSpecification seed, + AlternativeReqPredicate violatedPred, + Collection hiddenPredicates) { super(seed, violatedPred.getLocation(), seed.getSpecification()); - this.hiddenPredicates = new HashSet<>(); - this.contradictedPredicates = violatedPred.getAlternatives(); + this.hiddenPredicates = Set.copyOf(hiddenPredicates); + this.contradictedPredicates = List.copyOf(violatedPred.getAlternatives()); this.paramIndex = violatedPred.getParamIndex(); } - public void addHiddenPredicates(Collection hiddenPredicates) { - this.hiddenPredicates.addAll(hiddenPredicates); - } - public void mapPrecedingErrors() { for (HiddenPredicate hiddenPredicate : hiddenPredicates) { Collection precedingErrors = hiddenPredicate.getPrecedingErrors(); @@ -51,11 +51,6 @@ public void mapPrecedingErrors() { } } - /** - * This method returns a list of contradicting predicates - * - * @return list of contradicting predicates - */ public Collection getContradictedPredicates() { return contradictedPredicates; } @@ -85,64 +80,30 @@ public String toErrorMarkerString() { } private String getParamIndexAsText() { - String res; - switch (paramIndex) { - case -1: - return "Return value"; - case 0: - res = "First "; - break; - case 1: - res = "Second "; - break; - case 2: - res = "Third "; - break; - case 3: - res = "Fourth "; - break; - case 4: - res = "Fifth "; - break; - case 5: - res = "Sixth "; - break; - default: - res = (paramIndex + 1) + "th "; - break; - } - res += "parameter"; - return res; + return switch (paramIndex) { + case -1 -> "Return value"; + case 0 -> "First parameter"; + case 1 -> "Second parameter"; + case 2 -> "Third parameter"; + case 3 -> "Fourth parameter"; + case 4 -> "Fifth parameter"; + case 5 -> "Sixth parameter"; + default -> (paramIndex + 1) + "th parameter"; + }; } @Override public int hashCode() { - return Arrays.hashCode( - new Object[] { - super.hashCode(), hiddenPredicates, contradictedPredicates, paramIndex - }); + return Objects.hash(super.hashCode(), hiddenPredicates, contradictedPredicates, paramIndex); } @Override public boolean equals(Object obj) { - if (this == obj) return true; - if (!super.equals(obj)) return false; - if (getClass() != obj.getClass()) return false; - - RequiredPredicateError other = (RequiredPredicateError) obj; - if (hiddenPredicates == null) { - if (other.getHiddenPredicates() != null) return false; - } else if (!hiddenPredicates.equals(other.hiddenPredicates)) { - return false; - } - - if (contradictedPredicates == null) { - if (other.getContradictedPredicates() != null) return false; - } else if (!contradictedPredicates.equals(other.getContradictedPredicates())) { - return false; - } - - return paramIndex == other.getParamIndex(); + return super.equals(obj) + && obj instanceof RequiredPredicateError other + && Objects.equals(contradictedPredicates, other.getContradictedPredicates()) + && Objects.equals(hiddenPredicates, other.getHiddenPredicates()) + && paramIndex == other.getParamIndex(); } @Override diff --git a/CryptoAnalysis/src/main/java/crypto/constraints/ValueConstraint.java b/CryptoAnalysis/src/main/java/crypto/constraints/ValueConstraint.java index cac54f0de..7e10c8ed4 100644 --- a/CryptoAnalysis/src/main/java/crypto/constraints/ValueConstraint.java +++ b/CryptoAnalysis/src/main/java/crypto/constraints/ValueConstraint.java @@ -10,7 +10,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; public class ValueConstraint extends EvaluableConstraint { @@ -28,9 +27,7 @@ public void evaluate() { } List lowerCaseValues = - valCons.getValueRange().parallelStream() - .map(String::toLowerCase) - .collect(Collectors.toList()); + valCons.getValueRange().parallelStream().map(String::toLowerCase).toList(); for (Map.Entry val : values) { if (!lowerCaseValues.contains(val.getKey().toLowerCase())) { ConstraintError error = diff --git a/CryptoAnalysis/src/main/java/crypto/listener/AnalysisReporter.java b/CryptoAnalysis/src/main/java/crypto/listener/AnalysisReporter.java index 049f8ffd5..24f0476bc 100644 --- a/CryptoAnalysis/src/main/java/crypto/listener/AnalysisReporter.java +++ b/CryptoAnalysis/src/main/java/crypto/listener/AnalysisReporter.java @@ -6,6 +6,7 @@ import boomerang.scene.Statement; import boomerang.scene.Val; import com.google.common.collect.Multimap; +import crypto.analysis.AbstractPredicate; import crypto.analysis.EnsuredCrySLPredicate; import crypto.analysis.IAnalysisSeed; import crypto.analysis.errors.AbstractError; @@ -183,7 +184,7 @@ public void afterPredicateCheck() { public void onGeneratedPredicate( IAnalysisSeed fromSeed, - EnsuredCrySLPredicate predicate, + AbstractPredicate predicate, IAnalysisSeed toPred, Statement statement) { for (IResultsListener listener : resultsListeners) { @@ -229,53 +230,36 @@ public void ensuredPredicates( } public void reportError(IAnalysisSeed seed, AbstractError error) { - seed.setSecure(false); - for (IAnalysisListener analysisListener : analysisListeners) { analysisListener.onReportedError(seed, error); } for (IErrorListener errorListener : errorListeners) { - if (error instanceof CallToError) { - CallToError callToError = (CallToError) error; + if (error instanceof CallToError callToError) { errorListener.reportError(callToError); - } else if (error instanceof ConstraintError) { - ConstraintError constraintError = (ConstraintError) error; + } else if (error instanceof ConstraintError constraintError) { errorListener.reportError(constraintError); - } else if (error instanceof ForbiddenMethodError) { - ForbiddenMethodError forbiddenMethodError = (ForbiddenMethodError) error; + } else if (error instanceof ForbiddenMethodError forbiddenMethodError) { errorListener.reportError(forbiddenMethodError); - } else if (error instanceof HardCodedError) { - HardCodedError hardCodedError = (HardCodedError) error; + } else if (error instanceof HardCodedError hardCodedError) { errorListener.reportError(hardCodedError); - } else if (error instanceof ImpreciseValueExtractionError) { - ImpreciseValueExtractionError impreciseError = - (ImpreciseValueExtractionError) error; + } else if (error instanceof ImpreciseValueExtractionError impreciseError) { errorListener.reportError(impreciseError); - } else if (error instanceof IncompleteOperationError) { - IncompleteOperationError incompleteError = (IncompleteOperationError) error; + } else if (error instanceof IncompleteOperationError incompleteError) { errorListener.reportError(incompleteError); - } else if (error instanceof InstanceOfError) { - InstanceOfError instanceOfError = (InstanceOfError) error; + } else if (error instanceof InstanceOfError instanceOfError) { errorListener.reportError(instanceOfError); - } else if (error instanceof NeverTypeOfError) { - NeverTypeOfError neverTypeOfError = (NeverTypeOfError) error; + } else if (error instanceof NeverTypeOfError neverTypeOfError) { errorListener.reportError(neverTypeOfError); - } else if (error instanceof NoCallToError) { - NoCallToError noCallToError = (NoCallToError) error; + } else if (error instanceof NoCallToError noCallToError) { errorListener.reportError(noCallToError); - } else if (error instanceof PredicateContradictionError) { - PredicateContradictionError contradictionError = - (PredicateContradictionError) error; + } else if (error instanceof PredicateContradictionError contradictionError) { errorListener.reportError(contradictionError); - } else if (error instanceof RequiredPredicateError) { - RequiredPredicateError predicateError = (RequiredPredicateError) error; + } else if (error instanceof RequiredPredicateError predicateError) { errorListener.reportError(predicateError); - } else if (error instanceof TypestateError) { - TypestateError typestateError = (TypestateError) error; + } else if (error instanceof TypestateError typestateError) { errorListener.reportError(typestateError); - } else if (error instanceof UncaughtExceptionError) { - UncaughtExceptionError exceptionError = (UncaughtExceptionError) error; + } else if (error instanceof UncaughtExceptionError exceptionError) { errorListener.reportError(exceptionError); } else { errorListener.reportError(error); diff --git a/CryptoAnalysis/src/main/java/crypto/listener/IResultsListener.java b/CryptoAnalysis/src/main/java/crypto/listener/IResultsListener.java index aaef32b58..e49ee7fd8 100644 --- a/CryptoAnalysis/src/main/java/crypto/listener/IResultsListener.java +++ b/CryptoAnalysis/src/main/java/crypto/listener/IResultsListener.java @@ -5,6 +5,7 @@ import boomerang.scene.CallGraph; import boomerang.scene.Statement; import com.google.common.collect.Multimap; +import crypto.analysis.AbstractPredicate; import crypto.analysis.EnsuredCrySLPredicate; import crypto.analysis.IAnalysisSeed; import crypto.analysis.errors.AbstractError; @@ -36,7 +37,7 @@ void checkedConstraints( void generatedPredicate( IAnalysisSeed fromSeed, - EnsuredCrySLPredicate predicate, + AbstractPredicate predicate, IAnalysisSeed toSeed, Statement statement); diff --git a/CryptoAnalysis/src/test/java/test/UsagePatternResultsListener.java b/CryptoAnalysis/src/test/java/test/UsagePatternResultsListener.java index dbaa88eaa..f46171d4a 100644 --- a/CryptoAnalysis/src/test/java/test/UsagePatternResultsListener.java +++ b/CryptoAnalysis/src/test/java/test/UsagePatternResultsListener.java @@ -9,6 +9,7 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Table; +import crypto.analysis.AbstractPredicate; import crypto.analysis.EnsuredCrySLPredicate; import crypto.analysis.IAnalysisSeed; import crypto.analysis.errors.AbstractError; @@ -102,7 +103,7 @@ public void checkedConstraints( @Override public void generatedPredicate( IAnalysisSeed fromSeed, - EnsuredCrySLPredicate predicate, + AbstractPredicate predicate, IAnalysisSeed toSeed, Statement statement) { for (Assertion a : assertions) { diff --git a/CryptoAnalysis/src/test/java/test/assertions/HasEnsuredPredicateAssertion.java b/CryptoAnalysis/src/test/java/test/assertions/HasEnsuredPredicateAssertion.java index e274a512e..1487a1073 100644 --- a/CryptoAnalysis/src/test/java/test/assertions/HasEnsuredPredicateAssertion.java +++ b/CryptoAnalysis/src/test/java/test/assertions/HasEnsuredPredicateAssertion.java @@ -2,7 +2,7 @@ import boomerang.scene.Statement; import boomerang.scene.Val; -import crypto.analysis.EnsuredCrySLPredicate; +import crypto.analysis.AbstractPredicate; import crypto.analysis.HiddenPredicate; import java.util.Collection; import test.Assertion; @@ -38,7 +38,7 @@ public Statement getStmt() { return stmt; } - public void reported(Collection seed, EnsuredCrySLPredicate pred) { + public void reported(Collection seed, AbstractPredicate pred) { if (!seed.contains(val) || pred instanceof HiddenPredicate) { return; } diff --git a/CryptoAnalysis/src/test/java/test/assertions/HasGeneratedPredicateAssertion.java b/CryptoAnalysis/src/test/java/test/assertions/HasGeneratedPredicateAssertion.java index f07a3ca5c..824b90e05 100644 --- a/CryptoAnalysis/src/test/java/test/assertions/HasGeneratedPredicateAssertion.java +++ b/CryptoAnalysis/src/test/java/test/assertions/HasGeneratedPredicateAssertion.java @@ -2,7 +2,7 @@ import boomerang.scene.Statement; import boomerang.scene.Val; -import crypto.analysis.EnsuredCrySLPredicate; +import crypto.analysis.AbstractPredicate; import crypto.analysis.HiddenPredicate; import java.util.Collection; import test.Assertion; @@ -28,7 +28,7 @@ public boolean isImprecise() { return false; } - public void reported(Collection seed, EnsuredCrySLPredicate predicate) { + public void reported(Collection seed, AbstractPredicate predicate) { if (seed.contains(val) && !(predicate instanceof HiddenPredicate)) { satisfied = true; } diff --git a/CryptoAnalysis/src/test/java/test/assertions/HasNotGeneratedPredicateAssertion.java b/CryptoAnalysis/src/test/java/test/assertions/HasNotGeneratedPredicateAssertion.java index d4e309f83..5ee66eed2 100644 --- a/CryptoAnalysis/src/test/java/test/assertions/HasNotGeneratedPredicateAssertion.java +++ b/CryptoAnalysis/src/test/java/test/assertions/HasNotGeneratedPredicateAssertion.java @@ -2,7 +2,7 @@ import boomerang.scene.Statement; import boomerang.scene.Val; -import crypto.analysis.EnsuredCrySLPredicate; +import crypto.analysis.AbstractPredicate; import crypto.analysis.HiddenPredicate; import java.util.Collection; import test.Assertion; @@ -32,7 +32,7 @@ public Statement getStatement() { return statement; } - public void reported(Collection seed, EnsuredCrySLPredicate predicate) { + public void reported(Collection seed, AbstractPredicate predicate) { if (seed.contains(val) && !(predicate instanceof HiddenPredicate)) { imprecise = true; } diff --git a/CryptoAnalysis/src/test/java/test/assertions/NotHasEnsuredPredicateAssertion.java b/CryptoAnalysis/src/test/java/test/assertions/NotHasEnsuredPredicateAssertion.java index 55db56df8..6bce7e224 100644 --- a/CryptoAnalysis/src/test/java/test/assertions/NotHasEnsuredPredicateAssertion.java +++ b/CryptoAnalysis/src/test/java/test/assertions/NotHasEnsuredPredicateAssertion.java @@ -2,7 +2,7 @@ import boomerang.scene.Statement; import boomerang.scene.Val; -import crypto.analysis.EnsuredCrySLPredicate; +import crypto.analysis.AbstractPredicate; import crypto.analysis.HiddenPredicate; import java.util.Collection; import test.Assertion; @@ -38,7 +38,7 @@ public Statement getStmt() { return stmt; } - public void reported(Collection seed, EnsuredCrySLPredicate pred) { + public void reported(Collection seed, AbstractPredicate pred) { if (!seed.contains(val) || pred instanceof HiddenPredicate) { return; } diff --git a/HeadlessJavaScanner/src/test/java/scanner/setup/AbstractHeadlessTest.java b/HeadlessJavaScanner/src/test/java/scanner/setup/AbstractHeadlessTest.java index 658a3a687..7903d79b4 100644 --- a/HeadlessJavaScanner/src/test/java/scanner/setup/AbstractHeadlessTest.java +++ b/HeadlessJavaScanner/src/test/java/scanner/setup/AbstractHeadlessTest.java @@ -112,7 +112,7 @@ protected final void assertErrors( int difference = expected - actual; if (difference < 0) { report.add( - "\n\tFound " + "Found " + Math.abs(difference) + " too many errors of type " + errorType.getSimpleName() @@ -120,7 +120,7 @@ protected final void assertErrors( + methodWrapper); } else if (difference > 0) { report.add( - "\n\tFound " + "Found " + difference + " too few errors of type " + errorType.getSimpleName() @@ -148,7 +148,7 @@ protected final void assertErrors( int unexpectedErrors = getErrorsOfType(errorType, errors); report.add( - "\n\tFound " + "Found " + unexpectedErrors + " too many errors of type " + errorType.getSimpleName() diff --git a/HeadlessJavaScanner/src/test/java/scanner/targets/BragaCryptoGoodUsesTest.java b/HeadlessJavaScanner/src/test/java/scanner/targets/BragaCryptoGoodUsesTest.java index f01d1da5f..1f6f07e42 100644 --- a/HeadlessJavaScanner/src/test/java/scanner/targets/BragaCryptoGoodUsesTest.java +++ b/HeadlessJavaScanner/src/test/java/scanner/targets/BragaCryptoGoodUsesTest.java @@ -88,7 +88,7 @@ public void avoidCodingErrorsExamples() { new ErrorSpecification.Builder("example.PBEwLargeCountAndRandomSalt", "main", 1) .withTPs(ConstraintError.class, 1) .withTPs(TypestateError.class, 1) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 4) .withTPs(ForbiddenMethodError.class, 1) .withTPs(IncompleteOperationError.class, 1) .build()); @@ -123,7 +123,7 @@ public void avoidConstantPwdPBEExamples() { new ErrorSpecification.Builder("example.PBEwParameterPassword", "main", 1) .withTPs(ConstraintError.class, 1) .withTPs(TypestateError.class, 1) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 4) .withTPs(ForbiddenMethodError.class, 1) .withTPs(IncompleteOperationError.class, 1) .build()); @@ -391,7 +391,7 @@ public void avoidInsecureDefaultsExamples() { addErrorSpecification( new ErrorSpecification.Builder("example.UseQualifiedNameForPBE1", "main", 1) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 4) .withTPs(ForbiddenMethodError.class, 1) .withTPs(TypestateError.class, 1) .withTPs(IncompleteOperationError.class, 1) diff --git a/HeadlessJavaScanner/src/test/java/scanner/targets/BragaCryptoMisusesTest.java b/HeadlessJavaScanner/src/test/java/scanner/targets/BragaCryptoMisusesTest.java index ebae70404..279d60f0d 100644 --- a/HeadlessJavaScanner/src/test/java/scanner/targets/BragaCryptoMisusesTest.java +++ b/HeadlessJavaScanner/src/test/java/scanner/targets/BragaCryptoMisusesTest.java @@ -275,7 +275,7 @@ public void constPwd4PBEExamples() { addErrorSpecification( new ErrorSpecification.Builder("pkm.constPwd4PBE.ConstPassword4PBE1", "main", 1) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 4) .withTPs(TypestateError.class, 1) .withTPs(ForbiddenMethodError.class, 1) .withTPs(IncompleteOperationError.class, 1) @@ -284,7 +284,7 @@ public void constPwd4PBEExamples() { addErrorSpecification( new ErrorSpecification.Builder("pkm.constPwd4PBE.ConstPassword4PBE2", "main", 1) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 4) .withTPs(TypestateError.class, 1) .withTPs(ForbiddenMethodError.class, 1) .withTPs(IncompleteOperationError.class, 1) @@ -700,6 +700,7 @@ public void insecureDefaultExamples() { .withTPs(ConstraintError.class, 1) .withTPs(ForbiddenMethodError.class, 1) .withTPs(IncompleteOperationError.class, 1) + .withTPs(RequiredPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder("pdf.insecureDefault.InsecureDefault3DES", "main", 1) @@ -1184,7 +1185,7 @@ public void paramsPBEExamples() { addErrorSpecification( new ErrorSpecification.Builder("cib.paramsPBE.PBEwConstSalt1", "main", 1) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 4) .withTPs(ForbiddenMethodError.class, 1) .withTPs(TypestateError.class, 1) .withTPs(IncompleteOperationError.class, 1) @@ -1193,7 +1194,7 @@ public void paramsPBEExamples() { addErrorSpecification( new ErrorSpecification.Builder("cib.paramsPBE.PBEwSmallCount1", "main", 1) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 4) .withTPs(ForbiddenMethodError.class, 1) .withTPs(TypestateError.class, 1) .withTPs(IncompleteOperationError.class, 1) @@ -1202,7 +1203,7 @@ public void paramsPBEExamples() { addErrorSpecification( new ErrorSpecification.Builder("cib.paramsPBE.PBEwSmallSalt", "main", 1) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 4) .withTPs(ForbiddenMethodError.class, 1) .withTPs(TypestateError.class, 1) .withTPs(IncompleteOperationError.class, 1) @@ -1349,6 +1350,7 @@ public void riskyInsecureCryptoExamples() { .withTPs(ConstraintError.class, 2) .withTPs(ForbiddenMethodError.class, 1) .withTPs(IncompleteOperationError.class, 1) + .withTPs(RequiredPredicateError.class, 2) .build()); addErrorSpecification( From 8fdae8251f231aa834deffdbadb580974aa8d904 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Mon, 16 Dec 2024 12:14:52 +0100 Subject: [PATCH 16/32] Set style.yml to Java 17 --- .github/workflows/style.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 894f1638b..6ca3ce780 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -14,6 +14,13 @@ jobs: steps: - name: Checkout source code uses: actions/checkout@v4 + # Set up Java version + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + distribution: adopt + java-package: jdk + java-version: 17 # Restores Maven dependecies - name: Restore local Maven repository uses: actions/cache@v4 From f5ab379cd7a7f4fb87e26a3553f24d847afd96f7 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Mon, 16 Dec 2024 12:20:39 +0100 Subject: [PATCH 17/32] Remove old visualization --- .../java/crypto/analysis/CryptoScanner.java | 7 ---- .../java/crypto/analysis/SeedGenerator.java | 8 ---- .../crypto/typestate/TypestateAnalysis.java | 8 ++-- .../crypto/typestate/TypestateDefinition.java | 5 --- .../iem/scanner/HeadlessJavaScanner.java | 38 ------------------- 5 files changed, 3 insertions(+), 63 deletions(-) diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/CryptoScanner.java b/CryptoAnalysis/src/main/java/crypto/analysis/CryptoScanner.java index 4195b8e40..ac38d2798 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/CryptoScanner.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/CryptoScanner.java @@ -1,7 +1,5 @@ package crypto.analysis; -import boomerang.Query; -import boomerang.debugger.Debugger; import boomerang.scene.CallGraph; import boomerang.scene.DataFlowScope; import boomerang.scene.Method; @@ -28,7 +26,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import typestate.TransitionFunction; public abstract class CryptoScanner { @@ -175,10 +172,6 @@ protected DataFlowScope createDataFlowScope() { return new CryptoAnalysisDataFlowScope(ruleset, Collections.emptySet()); } - public Debugger debugger(Query query) { - return new Debugger<>(); - } - public SparseCFGCache.SparsificationStrategy getSparsificationStrategy() { return SparseCFGCache.SparsificationStrategy.NONE; } diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/SeedGenerator.java b/CryptoAnalysis/src/main/java/crypto/analysis/SeedGenerator.java index e14f4ef1f..669f08066 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/SeedGenerator.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/SeedGenerator.java @@ -1,6 +1,5 @@ package crypto.analysis; -import boomerang.debugger.Debugger; import boomerang.results.ForwardBoomerangResults; import boomerang.scene.CallGraph; import boomerang.scene.DataFlowScope; @@ -10,7 +9,6 @@ import crypto.typestate.TypestateAnalysis; import crypto.typestate.TypestateDefinition; import crysl.rule.CrySLRule; -import ideal.IDEALSeedSolver; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -42,12 +40,6 @@ public DataFlowScope getDataFlowScope() { return scanner.getDataFlowScope(); } - @Override - public Debugger getDebugger( - IDEALSeedSolver idealSeedSolver) { - return scanner.debugger(idealSeedSolver.getSeed()); - } - @Override public int getTimeout() { return scanner.getTimeout(); diff --git a/CryptoAnalysis/src/main/java/crypto/typestate/TypestateAnalysis.java b/CryptoAnalysis/src/main/java/crypto/typestate/TypestateAnalysis.java index bf041a297..b603adee6 100644 --- a/CryptoAnalysis/src/main/java/crypto/typestate/TypestateAnalysis.java +++ b/CryptoAnalysis/src/main/java/crypto/typestate/TypestateAnalysis.java @@ -46,11 +46,10 @@ public void runTypestateAnalysis() { Collection seeds = analysisScope.computeSeeds(); for (Query seed : seeds) { - if (!(seed instanceof ForwardSeedQuery)) { + if (!(seed instanceof ForwardSeedQuery query)) { continue; } - ForwardSeedQuery query = (ForwardSeedQuery) seed; runTypestateAnalysisForSeed(query); } } @@ -90,7 +89,7 @@ public CallGraph callGraph() { @Override public Debugger debugger( IDEALSeedSolver idealSeedSolver) { - return definition.getDebugger(idealSeedSolver); + return new Debugger<>(); } @Override @@ -118,11 +117,10 @@ public Map> getRes WeightedForwardQuery, ForwardBoomerangResults> entry : resultHandler.getResults().entrySet()) { - if (!(entry.getKey() instanceof ForwardSeedQuery)) { + if (!(entry.getKey() instanceof ForwardSeedQuery forwardSeedQuery)) { continue; } - ForwardSeedQuery forwardSeedQuery = (ForwardSeedQuery) entry.getKey(); results.put(forwardSeedQuery, entry.getValue()); } return results; diff --git a/CryptoAnalysis/src/main/java/crypto/typestate/TypestateDefinition.java b/CryptoAnalysis/src/main/java/crypto/typestate/TypestateDefinition.java index ed39b3f1c..5c7a76572 100644 --- a/CryptoAnalysis/src/main/java/crypto/typestate/TypestateDefinition.java +++ b/CryptoAnalysis/src/main/java/crypto/typestate/TypestateDefinition.java @@ -1,12 +1,9 @@ package crypto.typestate; -import boomerang.debugger.Debugger; import boomerang.scene.CallGraph; import boomerang.scene.DataFlowScope; import crysl.rule.CrySLRule; -import ideal.IDEALSeedSolver; import java.util.Collection; -import typestate.TransitionFunction; public interface TypestateDefinition { @@ -16,7 +13,5 @@ public interface TypestateDefinition { DataFlowScope getDataFlowScope(); - Debugger getDebugger(IDEALSeedSolver idealSeedSolver); - int getTimeout(); } diff --git a/HeadlessJavaScanner/src/main/java/de/fraunhofer/iem/scanner/HeadlessJavaScanner.java b/HeadlessJavaScanner/src/main/java/de/fraunhofer/iem/scanner/HeadlessJavaScanner.java index 98bd07208..3b9b038e8 100644 --- a/HeadlessJavaScanner/src/main/java/de/fraunhofer/iem/scanner/HeadlessJavaScanner.java +++ b/HeadlessJavaScanner/src/main/java/de/fraunhofer/iem/scanner/HeadlessJavaScanner.java @@ -1,8 +1,5 @@ package de.fraunhofer.iem.scanner; -import boomerang.Query; -import boomerang.debugger.Debugger; -import boomerang.debugger.IDEVizDebugger; import boomerang.scene.CallGraph; import boomerang.scene.DataFlowScope; import boomerang.scene.sparse.SparseCFGCache; @@ -17,14 +14,12 @@ import de.fraunhofer.iem.framework.SootSetup; import de.fraunhofer.iem.framework.SootUpSetup; import de.fraunhofer.iem.scanner.ScannerSettings.CallGraphAlgorithm; -import java.io.File; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import typestate.TransitionFunction; public class HeadlessJavaScanner extends CryptoScanner { @@ -68,39 +63,6 @@ public DataFlowScope createDataFlowScope() { return new CryptoAnalysisDataFlowScope(super.getRuleset(), getIgnoredSections()); } - @Override - public Debugger debugger(Query query) { - if (settings.isVisualization()) { - - if (settings.getReportDirectory() == null) { - LOGGER.error( - "The visualization requires the --reportPath option. Disabling visualization..."); - return new Debugger<>(); - } - - File vizFile = - new File( - settings.getReportDirectory() - + File.separator - + "viz" - + File.separator - + query.var().getVariableName() - + ".json"); - boolean created = vizFile.getParentFile().mkdirs(); - - if (!created) { - LOGGER.error( - "Could not create directory {}. Disabling visualization...", - vizFile.getAbsolutePath()); - return new Debugger<>(); - } - - return new IDEVizDebugger<>(vizFile); - } - - return new Debugger<>(); - } - @Override public SparseCFGCache.SparsificationStrategy getSparsificationStrategy() { switch (settings.getSparseStrategy()) { From 1dafa592e65f89c851dbf42a61039fe038181d04 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Mon, 16 Dec 2024 16:44:31 +0100 Subject: [PATCH 18/32] Create first version of visualization --- CryptoAnalysis/pom.xml | 5 + .../java/crypto/visualization/Visualizer.java | 105 ++++++++++++++++++ .../crypto/visualization/WrappedCluster.java | 55 +++++++++ .../crypto/visualization/WrappedLine.java | 45 ++++++++ .../crypto/visualization/WrappedNode.java | 68 ++++++++++++ .../iem/scanner/HeadlessJavaScanner.java | 10 ++ 6 files changed, 288 insertions(+) create mode 100644 CryptoAnalysis/src/main/java/crypto/visualization/Visualizer.java create mode 100644 CryptoAnalysis/src/main/java/crypto/visualization/WrappedCluster.java create mode 100644 CryptoAnalysis/src/main/java/crypto/visualization/WrappedLine.java create mode 100644 CryptoAnalysis/src/main/java/crypto/visualization/WrappedNode.java diff --git a/CryptoAnalysis/pom.xml b/CryptoAnalysis/pom.xml index f5f324c8c..fd13b0429 100644 --- a/CryptoAnalysis/pom.xml +++ b/CryptoAnalysis/pom.xml @@ -75,6 +75,11 @@ jackson-databind 2.18.2
+ + org.graphper + graph-support + 1.4.0 +
diff --git a/CryptoAnalysis/src/main/java/crypto/visualization/Visualizer.java b/CryptoAnalysis/src/main/java/crypto/visualization/Visualizer.java new file mode 100644 index 000000000..4f1a2359c --- /dev/null +++ b/CryptoAnalysis/src/main/java/crypto/visualization/Visualizer.java @@ -0,0 +1,105 @@ +package crypto.visualization; + +import crypto.analysis.IAnalysisSeed; +import crypto.analysis.errors.AbstractError; +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import org.graphper.api.Cluster; +import org.graphper.api.FileType; +import org.graphper.api.Graphviz; +import org.graphper.api.Line; +import org.graphper.api.Subgraph; +import org.graphper.api.attributes.Rank; +import org.graphper.draw.ExecuteException; + +public class Visualizer { + + private static final String VISUALIZATION_NAME = "visualization"; + + private final File outputFile; + + public Visualizer(String outputDir) throws IOException { + if (outputDir == null) { + throw new NullPointerException("OutputDir must not be null"); + } + + this.outputFile = new File(outputDir); + + if (!outputFile.exists()) { + throw new IOException("Directory " + outputFile.getAbsolutePath() + " does not exist"); + } + + if (!outputFile.isDirectory()) { + throw new IOException(outputFile.getAbsolutePath() + " is not a directory"); + } + } + + public void createVisualization(Collection seeds) + throws ExecuteException, IOException { + + Graphviz.GraphvizBuilder builder = Graphviz.digraph(); + + Map errorToNode = new HashMap<>(); + for (IAnalysisSeed seed : seeds) { + Map errorToNodeForSeed = new HashMap<>(); + + for (AbstractError error : seed.getErrors()) { + WrappedNode node = new WrappedNode(error); + + builder.addNode(node.asGraphicalNode()); + errorToNodeForSeed.put(error, node); + } + + if (!errorToNodeForSeed.isEmpty()) { + Cluster cluster = createClusterForSeed(seed, errorToNodeForSeed); + builder.cluster(cluster); + } + + errorToNode.putAll(errorToNodeForSeed); + } + + Collection lines = createLines(errorToNode); + for (Line line : lines) { + builder.addLine(line); + } + + Graphviz graphviz = builder.build(); + graphviz.toFile(FileType.PNG).save(outputFile.getAbsolutePath(), VISUALIZATION_NAME); + } + + private Cluster createClusterForSeed( + IAnalysisSeed seed, Map errorToNode) { + Subgraph.SubgraphBuilder subgraphBuilder = Subgraph.builder().rank(Rank.SAME); + + for (AbstractError error : errorToNode.keySet()) { + subgraphBuilder.addNode(errorToNode.get(error).asGraphicalNode()); + } + + Subgraph subgraph = subgraphBuilder.build(); + + return WrappedCluster.forSeed(seed, subgraph).asGraphicalCluster(); + } + + private Collection createLines(Map errorToNode) { + Collection result = new HashSet<>(); + + for (AbstractError error : errorToNode.keySet()) { + WrappedNode from = errorToNode.get(error); + + for (AbstractError subError : error.getSubsequentErrors()) { + if (errorToNode.containsKey(subError)) { + WrappedNode to = errorToNode.get(subError); + + Line line = WrappedLine.forLine(from, to).asGraphicalLine(); + result.add(line); + } + } + } + + return result; + } +} diff --git a/CryptoAnalysis/src/main/java/crypto/visualization/WrappedCluster.java b/CryptoAnalysis/src/main/java/crypto/visualization/WrappedCluster.java new file mode 100644 index 000000000..5f63135c2 --- /dev/null +++ b/CryptoAnalysis/src/main/java/crypto/visualization/WrappedCluster.java @@ -0,0 +1,55 @@ +package crypto.visualization; + +import crypto.analysis.IAnalysisSeed; +import java.util.Objects; +import org.graphper.api.Cluster; +import org.graphper.api.Subgraph; +import org.graphper.api.attributes.ClusterShapeEnum; +import org.graphper.api.attributes.Color; +import org.graphper.api.attributes.Labeljust; + +public class WrappedCluster { + + private final IAnalysisSeed seed; + private final Subgraph subgraph; + private Cluster cluster; + + private WrappedCluster(IAnalysisSeed seed, Subgraph subgraph) { + this.seed = seed; + this.subgraph = subgraph; + } + + public static WrappedCluster forSeed(IAnalysisSeed seed, Subgraph subgraph) { + return new WrappedCluster(seed, subgraph); + } + + public Cluster asGraphicalCluster() { + if (cluster == null) { + cluster = + Cluster.builder() + .subgraph(subgraph) + .label(seed.getFact().getVariableName()) + .labeljust(Labeljust.LEFT) + .shape(ClusterShapeEnum.RECT) + .bgColor(Color.GREY) + .build(); + } + + return cluster; + } + + @Override + public int hashCode() { + return Objects.hash(seed); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof WrappedCluster other && Objects.equals(seed, other.seed); + } + + @Override + public String toString() { + return "Cluster: " + seed; + } +} diff --git a/CryptoAnalysis/src/main/java/crypto/visualization/WrappedLine.java b/CryptoAnalysis/src/main/java/crypto/visualization/WrappedLine.java new file mode 100644 index 000000000..12bbab5d1 --- /dev/null +++ b/CryptoAnalysis/src/main/java/crypto/visualization/WrappedLine.java @@ -0,0 +1,45 @@ +package crypto.visualization; + +import java.util.Objects; +import org.graphper.api.Line; + +public class WrappedLine { + + private final WrappedNode from; + private final WrappedNode to; + private Line line; + + private WrappedLine(WrappedNode from, WrappedNode to) { + this.from = from; + this.to = to; + } + + public static WrappedLine forLine(WrappedNode from, WrappedNode to) { + return new WrappedLine(from, to); + } + + public Line asGraphicalLine() { + if (line == null) { + line = Line.builder(from.asGraphicalNode(), to.asGraphicalNode()).build(); + } + + return line; + } + + @Override + public int hashCode() { + return Objects.hash(from, to); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof WrappedLine other + && Objects.equals(from, other.from) + && Objects.equals(to, other.to); + } + + @Override + public String toString() { + return from + " -> " + to; + } +} diff --git a/CryptoAnalysis/src/main/java/crypto/visualization/WrappedNode.java b/CryptoAnalysis/src/main/java/crypto/visualization/WrappedNode.java new file mode 100644 index 000000000..4b60563c8 --- /dev/null +++ b/CryptoAnalysis/src/main/java/crypto/visualization/WrappedNode.java @@ -0,0 +1,68 @@ +package crypto.visualization; + +import crypto.analysis.errors.AbstractConstraintsError; +import crypto.analysis.errors.AbstractError; +import java.util.Objects; + +import crypto.analysis.errors.AbstractOrderError; +import crypto.analysis.errors.ForbiddenMethodError; +import crypto.analysis.errors.PredicateConstraintError; +import org.graphper.api.Node; +import org.graphper.api.attributes.Color; +import org.graphper.api.attributes.NodeShapeEnum; + +public class WrappedNode { + + private final AbstractError error; + private Node graphicalNode; + + public WrappedNode(AbstractError error) { + this.error = error; + } + + public Node asGraphicalNode() { + if (graphicalNode == null) { + graphicalNode = + Node.builder() + .label(getLabel()) + .shape(NodeShapeEnum.RECT) + .fillColor(getColor()) + .build(); + } + + return graphicalNode; + } + + private String getLabel() { + return error.getClass().getSimpleName() + "\n" + error.getErrorStatement() + "\nLine: " + error.getLineNumber(); + } + + private Color getColor() { + if (error instanceof ForbiddenMethodError) { + return Color.BLUE; + } else if (error instanceof AbstractOrderError) { + return Color.ORANGE; + } else if (error instanceof AbstractConstraintsError) { + return Color.YELLOW; + } else if (error instanceof PredicateConstraintError) { + return Color.INDIGO; + } else { + return Color.WHITE; + } + } + + @Override + public int hashCode() { + return Objects.hash(error); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof WrappedNode other && Objects.equals(error, other.error); + } + + @Override + public String toString() { + return "Node: " + error; + } +} diff --git a/HeadlessJavaScanner/src/main/java/de/fraunhofer/iem/scanner/HeadlessJavaScanner.java b/HeadlessJavaScanner/src/main/java/de/fraunhofer/iem/scanner/HeadlessJavaScanner.java index 3b9b038e8..d7b5796f1 100644 --- a/HeadlessJavaScanner/src/main/java/de/fraunhofer/iem/scanner/HeadlessJavaScanner.java +++ b/HeadlessJavaScanner/src/main/java/de/fraunhofer/iem/scanner/HeadlessJavaScanner.java @@ -9,15 +9,18 @@ import crypto.exceptions.CryptoAnalysisParserException; import crypto.reporting.Reporter; import crypto.reporting.ReporterFactory; +import crypto.visualization.Visualizer; import de.fraunhofer.iem.framework.FrameworkSetup; import de.fraunhofer.iem.framework.OpalSetup; import de.fraunhofer.iem.framework.SootSetup; import de.fraunhofer.iem.framework.SootUpSetup; import de.fraunhofer.iem.scanner.ScannerSettings.CallGraphAlgorithm; +import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Set; +import org.graphper.draw.ExecuteException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -105,6 +108,13 @@ public void run() { reporter.createAnalysisReport( super.getDiscoveredSeeds(), super.getCollectedErrors(), super.getStatistics()); } + + try { + Visualizer visualizer = new Visualizer(getReportDirectory()); + visualizer.createVisualization(super.getDiscoveredSeeds()); + } catch (IOException | ExecuteException e) { + LOGGER.error("Couldn't create visualization: " + e.getMessage()); + } } private FrameworkSetup setupFramework() { From 2a55fd57072255757866a93a49719f5520e8f8f0 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Mon, 16 Dec 2024 16:46:48 +0100 Subject: [PATCH 19/32] Fix path to JCA in release action --- .github/workflows/deploy_and_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy_and_release.yml b/.github/workflows/deploy_and_release.yml index cc5e0835f..a3b8731db 100644 --- a/.github/workflows/deploy_and_release.yml +++ b/.github/workflows/deploy_and_release.yml @@ -77,7 +77,7 @@ jobs: - name: Zip JavaCryptographicArchitecture folder run: | echo "Zipping the JavaCryptographicArchitecture folder..." - zip -r JavaCryptographicArchitecture.zip CryptoAnalysis/src/main/resources/JavaCryptographicArchitecture + zip -r JavaCryptographicArchitecture.zip CryptoAnalysis/src/test/resources/rules/JavaCryptographicArchitecture - name: Create GitHub Release run: | From 0f5fae0a9956bfa570dc336b47e5a01f445ddbf5 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Tue, 17 Dec 2024 14:25:38 +0100 Subject: [PATCH 20/32] Add first version of visualizer --- .../java/crypto/analysis/CryptoScanner.java | 44 ++++++ .../main/java/crypto/reporting/Reporter.java | 4 +- .../java/crypto/visualization/Visualizer.java | 6 + .../crypto/visualization/WrappedNode.java | 9 +- .../iem/android/AndroidSettings.java | 43 ++++-- .../iem/android/HeadlessAndroidScanner.java | 19 +-- .../test/java/settings/CommandLineTest.java | 37 +++++ .../src/test/java/settings/ProgramTest.java | 9 ++ .../iem/scanner/HeadlessJavaScanner.java | 68 +++------ .../iem/scanner/ScannerSettings.java | 141 ++++++++---------- .../scanner/targets/ReportFormatTest.java | 19 +++ .../test/java/settings/CommandLineTest.java | 17 +++ 12 files changed, 265 insertions(+), 151 deletions(-) diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/CryptoScanner.java b/CryptoAnalysis/src/main/java/crypto/analysis/CryptoScanner.java index ac38d2798..5dc14552c 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/CryptoScanner.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/CryptoScanner.java @@ -15,6 +15,9 @@ import crypto.listener.IAnalysisListener; import crypto.listener.IErrorListener; import crypto.listener.IResultsListener; +import crypto.reporting.Reporter; +import crypto.reporting.ReporterFactory; +import crypto.visualization.Visualizer; import crysl.CrySLParser; import crysl.rule.CrySLRule; import java.io.IOException; @@ -26,6 +29,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.graphper.draw.ExecuteException; public abstract class CryptoScanner { @@ -109,6 +113,46 @@ protected final void scan() { analysisReporter.afterAnalysis(); } + protected final void createReports( + Collection formats, + String reportDirectory, + boolean visualization) { + if (reportDirectory == null + && formats.stream() + .anyMatch( + e -> + Set.of( + Reporter.ReportFormat.TXT, + Reporter.ReportFormat.CSV, + Reporter.ReportFormat.CSV_SUMMARY, + Reporter.ReportFormat.SARIF) + .contains(e))) { + throw new RuntimeException("Cannot create report without existing report directory"); + } + + if (visualization && reportDirectory == null) { + throw new RuntimeException( + "Cannot create visualization without existing report directory"); + } + + Collection reporters = + ReporterFactory.createReporters(formats, reportDirectory, ruleset); + for (Reporter reporter : reporters) { + reporter.createAnalysisReport( + getDiscoveredSeeds(), getCollectedErrors(), getStatistics()); + } + + if (visualization) { + try { + Visualizer visualizer = new Visualizer(reportDirectory); + visualizer.createVisualization(getDiscoveredSeeds()); + } catch (IOException | ExecuteException e) { + throw new CryptoAnalysisException( + "Couldn't create visualization: " + e.getMessage()); + } + } + } + public final Collection getRuleset() { return ruleset; } diff --git a/CryptoAnalysis/src/main/java/crypto/reporting/Reporter.java b/CryptoAnalysis/src/main/java/crypto/reporting/Reporter.java index 8fdd3d7ed..74b7fe133 100644 --- a/CryptoAnalysis/src/main/java/crypto/reporting/Reporter.java +++ b/CryptoAnalysis/src/main/java/crypto/reporting/Reporter.java @@ -33,9 +33,9 @@ public enum ReportFormat { protected Reporter(String outputDir, Collection ruleset) throws IOException { if (outputDir == null) { - throw new RuntimeException( - "Cannot create report without directory (try using --reportDir or setOutputDirectory)"); + throw new RuntimeException("Cannot create report without report directory"); } + this.outputFile = new File(outputDir); this.ruleset = ruleset; diff --git a/CryptoAnalysis/src/main/java/crypto/visualization/Visualizer.java b/CryptoAnalysis/src/main/java/crypto/visualization/Visualizer.java index 4f1a2359c..e8022fbb0 100644 --- a/CryptoAnalysis/src/main/java/crypto/visualization/Visualizer.java +++ b/CryptoAnalysis/src/main/java/crypto/visualization/Visualizer.java @@ -15,9 +15,12 @@ import org.graphper.api.Subgraph; import org.graphper.api.attributes.Rank; import org.graphper.draw.ExecuteException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class Visualizer { + private static final Logger LOGGER = LoggerFactory.getLogger(Visualizer.class); private static final String VISUALIZATION_NAME = "visualization"; private final File outputFile; @@ -69,6 +72,9 @@ public void createVisualization(Collection seeds) Graphviz graphviz = builder.build(); graphviz.toFile(FileType.PNG).save(outputFile.getAbsolutePath(), VISUALIZATION_NAME); + LOGGER.info( + "Written visualization to {}", + outputFile.getAbsolutePath() + File.separator + VISUALIZATION_NAME + ".png"); } private Cluster createClusterForSeed( diff --git a/CryptoAnalysis/src/main/java/crypto/visualization/WrappedNode.java b/CryptoAnalysis/src/main/java/crypto/visualization/WrappedNode.java index 4b60563c8..9cef949f4 100644 --- a/CryptoAnalysis/src/main/java/crypto/visualization/WrappedNode.java +++ b/CryptoAnalysis/src/main/java/crypto/visualization/WrappedNode.java @@ -2,11 +2,10 @@ import crypto.analysis.errors.AbstractConstraintsError; import crypto.analysis.errors.AbstractError; -import java.util.Objects; - import crypto.analysis.errors.AbstractOrderError; import crypto.analysis.errors.ForbiddenMethodError; import crypto.analysis.errors.PredicateConstraintError; +import java.util.Objects; import org.graphper.api.Node; import org.graphper.api.attributes.Color; import org.graphper.api.attributes.NodeShapeEnum; @@ -34,7 +33,11 @@ public Node asGraphicalNode() { } private String getLabel() { - return error.getClass().getSimpleName() + "\n" + error.getErrorStatement() + "\nLine: " + error.getLineNumber(); + return error.getClass().getSimpleName() + + "\n" + + error.getErrorStatement() + + "\nLine: " + + error.getLineNumber(); } private Color getColor() { diff --git a/HeadlessAndroidScanner/src/main/java/de/fraunhofer/iem/android/AndroidSettings.java b/HeadlessAndroidScanner/src/main/java/de/fraunhofer/iem/android/AndroidSettings.java index 5d799608e..250dbcef4 100644 --- a/HeadlessAndroidScanner/src/main/java/de/fraunhofer/iem/android/AndroidSettings.java +++ b/HeadlessAndroidScanner/src/main/java/de/fraunhofer/iem/android/AndroidSettings.java @@ -4,7 +4,7 @@ import crypto.reporting.Reporter; import java.util.Collection; import java.util.HashSet; -import java.util.List; +import java.util.Set; import java.util.concurrent.Callable; import picocli.CommandLine; @@ -44,10 +44,15 @@ public class AndroidSettings implements Callable { + " Multiple formats should be split with a comma (e.g. CMD,TXT,CSV)") private String[] reportFormat = null; + @CommandLine.Option( + names = {"--visualization"}, + description = "Visualize the errors (requires --reportPath to be set)") + private boolean visualization = false; + private Collection reportFormats; public AndroidSettings() { - reportFormats = new HashSet<>(List.of(Reporter.ReportFormat.CMD)); + reportFormats = Set.of(Reporter.ReportFormat.CMD); } public void parseSettingsFromCLI(String[] settings) throws CryptoAnalysisParserException { @@ -56,7 +61,12 @@ public void parseSettingsFromCLI(String[] settings) throws CryptoAnalysisParserE int exitCode = parser.execute(settings); if (reportFormat != null) { - parseReportFormatValues(reportFormat); + reportFormats = parseReportFormatValues(reportFormat); + } + + if (visualization && reportPath == null) { + throw new CryptoAnalysisParserException( + "If visualization is enabled, the reportPath has to be set"); } if (exitCode != CommandLine.ExitCode.OK) { @@ -64,30 +74,31 @@ public void parseSettingsFromCLI(String[] settings) throws CryptoAnalysisParserE } } - private void parseReportFormatValues(String[] settings) throws CryptoAnalysisParserException { - reportFormats.clear(); + private Collection parseReportFormatValues(String[] settings) + throws CryptoAnalysisParserException { + Collection formats = new HashSet<>(); for (String format : settings) { String reportFormatValue = format.toLowerCase(); switch (reportFormatValue) { case "cmd": - reportFormats.add(Reporter.ReportFormat.CMD); + formats.add(Reporter.ReportFormat.CMD); break; case "txt": - reportFormats.add(Reporter.ReportFormat.TXT); + formats.add(Reporter.ReportFormat.TXT); break; case "sarif": - reportFormats.add(Reporter.ReportFormat.SARIF); + formats.add(Reporter.ReportFormat.SARIF); break; case "csv": - reportFormats.add(Reporter.ReportFormat.CSV); + formats.add(Reporter.ReportFormat.CSV); break; case "csv_summary": - reportFormats.add(Reporter.ReportFormat.CSV_SUMMARY); + formats.add(Reporter.ReportFormat.CSV_SUMMARY); break; case "github_annotation": - reportFormats.add(Reporter.ReportFormat.GITHUB_ANNOTATION); + formats.add(Reporter.ReportFormat.GITHUB_ANNOTATION); break; default: throw new CryptoAnalysisParserException( @@ -97,6 +108,8 @@ private void parseReportFormatValues(String[] settings) throws CryptoAnalysisPar + "Available options are: CMD, TXT, SARIF, CSV and CSV_SUMMARY.\n"); } } + + return formats; } public String getApkFile() { @@ -139,6 +152,14 @@ public void setReportPath(String reportPath) { this.reportPath = reportPath; } + public boolean isVisualization() { + return visualization; + } + + public void setVisualization(boolean visualization) { + this.visualization = visualization; + } + @Override public Integer call() throws Exception { return 0; diff --git a/HeadlessAndroidScanner/src/main/java/de/fraunhofer/iem/android/HeadlessAndroidScanner.java b/HeadlessAndroidScanner/src/main/java/de/fraunhofer/iem/android/HeadlessAndroidScanner.java index 9667918b8..b6bbd402a 100644 --- a/HeadlessAndroidScanner/src/main/java/de/fraunhofer/iem/android/HeadlessAndroidScanner.java +++ b/HeadlessAndroidScanner/src/main/java/de/fraunhofer/iem/android/HeadlessAndroidScanner.java @@ -5,7 +5,6 @@ import crypto.analysis.CryptoScanner; import crypto.exceptions.CryptoAnalysisParserException; import crypto.reporting.Reporter; -import crypto.reporting.ReporterFactory; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -58,20 +57,14 @@ public void run() { flowDroidSetup.setupFlowDroid(); additionalFrameworkSetup(); - // Initialize fields and reporters + // Initialize fields super.initialize(); - Collection reporters = - ReporterFactory.createReporters( - getReportFormats(), getReportDirectory(), super.getRuleset()); // Run the analysis super.scan(); // Report the errors - for (Reporter reporter : reporters) { - reporter.createAnalysisReport( - super.getDiscoveredSeeds(), super.getCollectedErrors(), super.getStatistics()); - } + super.createReports(getReportFormats(), getReportDirectory(), isVisualization()); } public String getApkFile() { @@ -102,5 +95,13 @@ public void setReportDirectory(String reportDirectory) { settings.setReportPath(reportDirectory); } + public boolean isVisualization() { + return settings.isVisualization(); + } + + public void setVisualization(boolean visualization) { + settings.setVisualization(visualization); + } + public void additionalFrameworkSetup() {} } diff --git a/HeadlessAndroidScanner/src/test/java/settings/CommandLineTest.java b/HeadlessAndroidScanner/src/test/java/settings/CommandLineTest.java index 41c561ee1..18080a552 100644 --- a/HeadlessAndroidScanner/src/test/java/settings/CommandLineTest.java +++ b/HeadlessAndroidScanner/src/test/java/settings/CommandLineTest.java @@ -21,6 +21,8 @@ public class CommandLineTest { private static final String REPORT_PATH = "--reportPath"; private static final String REPORT_FORMAT = "--reportFormat"; + private static final String VISUALIZATION = "--visualization"; + @Test public void testMinimalApplication() { String[] args = @@ -199,4 +201,39 @@ public void testReportFormat() { Reporter.ReportFormat.TXT, Reporter.ReportFormat.CSV)); } + + @Test + public void testVisualization() { + String reportPath = "path/to/report"; + String[] args = + new String[] { + APK_PATH, + EXAMPLE_APK_PATH, + PLATFORM_PATH, + EXAMPLE_PLATFORM_PATH, + RULES_DIR, + EXAMPLE_RULES_DIR, + VISUALIZATION, + REPORT_PATH, + reportPath + }; + HeadlessAndroidScanner scanner = HeadlessAndroidScanner.createFromCLISettings(args); + Assert.assertTrue(scanner.isVisualization()); + } + + @Test(expected = CryptoAnalysisParserException.class) + public void testInvalidVisualization() { + String[] args = + new String[] { + APK_PATH, + EXAMPLE_APK_PATH, + PLATFORM_PATH, + EXAMPLE_PLATFORM_PATH, + RULES_DIR, + EXAMPLE_RULES_DIR, + VISUALIZATION, + }; + HeadlessAndroidScanner scanner = HeadlessAndroidScanner.createFromCLISettings(args); + Assert.assertTrue(scanner.isVisualization()); + } } diff --git a/HeadlessAndroidScanner/src/test/java/settings/ProgramTest.java b/HeadlessAndroidScanner/src/test/java/settings/ProgramTest.java index d093e8d67..dfbc34f9d 100644 --- a/HeadlessAndroidScanner/src/test/java/settings/ProgramTest.java +++ b/HeadlessAndroidScanner/src/test/java/settings/ProgramTest.java @@ -87,4 +87,13 @@ public void testReportFormat() { Reporter.ReportFormat.TXT, Reporter.ReportFormat.CSV)); } + + @Test + public void testVisualization() { + HeadlessAndroidScanner scanner = + new HeadlessAndroidScanner( + EXAMPLE_APK_PATH, EXAMPLE_PLATFORM_PATH, EXAMPLE_RULES_DIR); + scanner.setVisualization(true); + Assert.assertTrue(scanner.isVisualization()); + } } diff --git a/HeadlessJavaScanner/src/main/java/de/fraunhofer/iem/scanner/HeadlessJavaScanner.java b/HeadlessJavaScanner/src/main/java/de/fraunhofer/iem/scanner/HeadlessJavaScanner.java index d7b5796f1..ad092ba80 100644 --- a/HeadlessJavaScanner/src/main/java/de/fraunhofer/iem/scanner/HeadlessJavaScanner.java +++ b/HeadlessJavaScanner/src/main/java/de/fraunhofer/iem/scanner/HeadlessJavaScanner.java @@ -5,22 +5,16 @@ import boomerang.scene.sparse.SparseCFGCache; import crypto.analysis.CryptoAnalysisDataFlowScope; import crypto.analysis.CryptoScanner; -import crypto.exceptions.CryptoAnalysisException; import crypto.exceptions.CryptoAnalysisParserException; import crypto.reporting.Reporter; -import crypto.reporting.ReporterFactory; -import crypto.visualization.Visualizer; import de.fraunhofer.iem.framework.FrameworkSetup; import de.fraunhofer.iem.framework.OpalSetup; import de.fraunhofer.iem.framework.SootSetup; import de.fraunhofer.iem.framework.SootUpSetup; import de.fraunhofer.iem.scanner.ScannerSettings.CallGraphAlgorithm; -import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; -import java.util.Set; -import org.graphper.draw.ExecuteException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,24 +57,16 @@ protected CallGraph constructCallGraph() { @Override public DataFlowScope createDataFlowScope() { - return new CryptoAnalysisDataFlowScope(super.getRuleset(), getIgnoredSections()); + return new CryptoAnalysisDataFlowScope(super.getRuleset(), settings.getIgnoredSections()); } @Override public SparseCFGCache.SparsificationStrategy getSparsificationStrategy() { - switch (settings.getSparseStrategy()) { - case NONE: - return SparseCFGCache.SparsificationStrategy.NONE; - case TYPE_BASED: - return SparseCFGCache.SparsificationStrategy.TYPE_BASED; - case ALIAS_AWARE: - return SparseCFGCache.SparsificationStrategy.ALIAS_AWARE; - default: - LOGGER.error( - "Could not set sparsification strategy {}. Defaulting to NONE...", - settings.getSparseStrategy()); - return SparseCFGCache.SparsificationStrategy.NONE; - } + return switch (settings.getSparseStrategy()) { + case NONE -> SparseCFGCache.SparsificationStrategy.NONE; + case TYPE_BASED -> SparseCFGCache.SparsificationStrategy.TYPE_BASED; + case ALIAS_AWARE -> SparseCFGCache.SparsificationStrategy.ALIAS_AWARE; + }; } @Override @@ -94,44 +80,26 @@ public void run() { frameworkSetup.initializeFramework(); additionalFrameworkSetup(); - // Initialize fields and reporters + // Initialize fields super.initialize(); - Collection reporters = - ReporterFactory.createReporters( - getReportFormats(), getReportDirectory(), super.getRuleset()); // Run the analysis super.scan(); // Report the errors - for (Reporter reporter : reporters) { - reporter.createAnalysisReport( - super.getDiscoveredSeeds(), super.getCollectedErrors(), super.getStatistics()); - } - - try { - Visualizer visualizer = new Visualizer(getReportDirectory()); - visualizer.createVisualization(super.getDiscoveredSeeds()); - } catch (IOException | ExecuteException e) { - LOGGER.error("Couldn't create visualization: " + e.getMessage()); - } + super.createReports(getReportFormats(), getReportDirectory(), isVisualization()); } private FrameworkSetup setupFramework() { - switch (settings.getFramework()) { - case SOOT: - return new SootSetup( - settings.getApplicationPath(), - settings.getCallGraph(), - settings.getSootPath()); - case SOOT_UP: - return new SootUpSetup(settings.getApplicationPath(), settings.getCallGraph()); - case OPAL: - return new OpalSetup(settings.getApplicationPath(), settings.getCallGraph()); - default: - throw new CryptoAnalysisException( - "Framework " + settings.getFramework().name() + " is not supported"); - } + return switch (settings.getFramework()) { + case SOOT -> + new SootSetup( + settings.getApplicationPath(), + settings.getCallGraph(), + settings.getSootPath()); + case SOOT_UP -> new SootUpSetup(settings.getApplicationPath(), settings.getCallGraph()); + case OPAL -> new OpalSetup(settings.getApplicationPath(), settings.getCallGraph()); + }; } public String getApplicationPath() { @@ -170,7 +138,7 @@ public void setReportDirectory(String reportDirectory) { settings.setReportDirectory(reportDirectory); } - public Set getReportFormats() { + public Collection getReportFormats() { return settings.getReportFormats(); } diff --git a/HeadlessJavaScanner/src/main/java/de/fraunhofer/iem/scanner/ScannerSettings.java b/HeadlessJavaScanner/src/main/java/de/fraunhofer/iem/scanner/ScannerSettings.java index 1e36ea824..ef3fe0d0c 100644 --- a/HeadlessJavaScanner/src/main/java/de/fraunhofer/iem/scanner/ScannerSettings.java +++ b/HeadlessJavaScanner/src/main/java/de/fraunhofer/iem/scanner/ScannerSettings.java @@ -64,7 +64,7 @@ public class ScannerSettings implements Callable { @CommandLine.Option( names = {"--visualization"}, - description = "Enable visualization") + description = "Visualize the errors (requires --reportPath to be set)") private boolean visualization = false; @CommandLine.Option( @@ -112,13 +112,13 @@ public enum SparseStrategy { private CallGraphAlgorithm callGraphAlgorithm; private Framework framework; - private Set reportFormats; + private Collection reportFormats; private Collection ignoredSections; private SparseStrategy sparseStrategy; public ScannerSettings() { callGraphAlgorithm = CallGraphAlgorithm.CHA; - reportFormats = new HashSet<>(List.of(Reporter.ReportFormat.CMD)); + reportFormats = Set.of(Reporter.ReportFormat.CMD); framework = Framework.SOOT; ignoredSections = new ArrayList<>(); sparseStrategy = SparseStrategy.NONE; @@ -130,23 +130,28 @@ public void parseSettingsFromCLI(String[] settings) throws CryptoAnalysisParserE int exitCode = parser.execute(settings); if (frameworkOption != null) { - parseFrameworkOption(frameworkOption); + framework = parseFrameworkOption(frameworkOption); } if (cg != null) { - parseControlGraphOption(cg); + callGraphAlgorithm = parseCallGraphOption(cg); } if (reportFormat != null) { - parseReportFormatValues(reportFormat); + reportFormats = parseReportFormatOption(reportFormat); } if (ignoreSectionsPath != null) { - parseIgnoredSections(ignoreSectionsPath); + ignoredSections = parseIgnoredSectionOption(ignoreSectionsPath); + } + + if (visualization && reportPath == null) { + throw new CryptoAnalysisParserException( + "If visualization is enabled, the reportPath has to be set"); } if (sparseStrategyInput != null) { - parseSparseStrategy(sparseStrategyInput); + sparseStrategy = parseSparseStrategyOption(sparseStrategyInput); } if (timeout < 0) { @@ -158,89 +163,77 @@ public void parseSettingsFromCLI(String[] settings) throws CryptoAnalysisParserE } } - private void parseFrameworkOption(String option) throws CryptoAnalysisParserException { + private Framework parseFrameworkOption(String option) throws CryptoAnalysisParserException { String frameworkValue = option.toLowerCase(); - switch (frameworkValue) { - case "soot": - framework = Framework.SOOT; - break; - case "soot_up": - framework = Framework.SOOT_UP; - break; - case "opal": - framework = Framework.OPAL; - break; - default: - throw new CryptoAnalysisParserException( - "Framework " + option + " is not supported"); - } + return switch (frameworkValue) { + case "soot" -> Framework.SOOT; + case "soot_up" -> Framework.SOOT_UP; + case "opal" -> Framework.OPAL; + default -> + throw new CryptoAnalysisParserException( + "Incorrect framework option: " + option); + }; } - private void parseControlGraphOption(String value) throws CryptoAnalysisParserException { - String CGValue = value.toLowerCase(); - - switch (CGValue) { - case "cha": - callGraphAlgorithm = CallGraphAlgorithm.CHA; - break; - case "spark": - callGraphAlgorithm = CallGraphAlgorithm.SPARK; - break; - case "sparklib": - callGraphAlgorithm = CallGraphAlgorithm.SPARK_LIB; - break; - default: - throw new CryptoAnalysisParserException( - "Incorrect value " - + CGValue - + " for --cg option. " - + "Available options are: CHA, SPARK and SPARKLIB.\n"); - } + private CallGraphAlgorithm parseCallGraphOption(String option) + throws CryptoAnalysisParserException { + String callGraphValue = option.toLowerCase(); + + return switch (callGraphValue) { + case "cha" -> CallGraphAlgorithm.CHA; + case "spark" -> CallGraphAlgorithm.SPARK; + case "sparklib" -> CallGraphAlgorithm.SPARK_LIB; + default -> + throw new CryptoAnalysisParserException( + "Incorrect call graph value: " + option); + }; } - private void parseReportFormatValues(String[] settings) throws CryptoAnalysisParserException { - reportFormats.clear(); + private Collection parseReportFormatOption(String[] settings) + throws CryptoAnalysisParserException { + Collection formats = new HashSet<>(); for (String format : settings) { String reportFormatValue = format.toLowerCase(); switch (reportFormatValue) { case "cmd": - reportFormats.add(Reporter.ReportFormat.CMD); + formats.add(Reporter.ReportFormat.CMD); break; case "txt": - reportFormats.add(Reporter.ReportFormat.TXT); + formats.add(Reporter.ReportFormat.TXT); break; case "sarif": - reportFormats.add(Reporter.ReportFormat.SARIF); + formats.add(Reporter.ReportFormat.SARIF); break; case "csv": - reportFormats.add(Reporter.ReportFormat.CSV); + formats.add(Reporter.ReportFormat.CSV); break; case "csv_summary": - reportFormats.add(Reporter.ReportFormat.CSV_SUMMARY); + formats.add(Reporter.ReportFormat.CSV_SUMMARY); break; case "github_annotation": - reportFormats.add(Reporter.ReportFormat.GITHUB_ANNOTATION); + formats.add(Reporter.ReportFormat.GITHUB_ANNOTATION); break; default: throw new CryptoAnalysisParserException( - "Incorrect value " - + reportFormatValue - + " for --reportFormat option. " - + "Available options are: CMD, TXT, SARIF, CSV and CSV_SUMMARY.\n"); + "Incorrect report format value: " + format); } } + + return formats; } - private void parseIgnoredSections(String path) throws CryptoAnalysisParserException { - final File ignorePackageFile = new File(path); + private Collection parseIgnoredSectionOption(String path) + throws CryptoAnalysisParserException { + Collection result = new ArrayList<>(); + File ignorePackageFile = new File(path); if (ignorePackageFile.isFile() && ignorePackageFile.canRead()) { try { List lines = Files.readLines(ignorePackageFile, Charset.defaultCharset()); - ignoredSections.addAll(lines); + result.addAll(lines); } catch (IOException e) { throw new CryptoAnalysisParserException( "Error while reading file " + ignorePackageFile + ": " + e.getMessage()); @@ -249,25 +242,21 @@ private void parseIgnoredSections(String path) throws CryptoAnalysisParserExcept throw new CryptoAnalysisParserException( ignorePackageFile + " is not a file or cannot be read"); } + + return result; } - private void parseSparseStrategy(String strategy) { - String strategyLowerCase = strategy.toLowerCase(); - - switch (strategyLowerCase) { - case "none": - sparseStrategy = SparseStrategy.NONE; - break; - case "type_based": - sparseStrategy = SparseStrategy.TYPE_BASED; - break; - case "alias_aware": - sparseStrategy = SparseStrategy.ALIAS_AWARE; - break; - default: - throw new CryptoAnalysisParserException( - sparseStrategy + " is not a valid sparsification strategy"); - } + private SparseStrategy parseSparseStrategyOption(String option) { + String strategyLowerCase = option.toLowerCase(); + + return switch (strategyLowerCase) { + case "none" -> SparseStrategy.NONE; + case "type_based" -> SparseStrategy.TYPE_BASED; + case "alias_aware" -> SparseStrategy.ALIAS_AWARE; + default -> + throw new CryptoAnalysisParserException( + "Incorrect sparsification strategy: " + option); + }; } public String getApplicationPath() { @@ -318,7 +307,7 @@ public void setReportDirectory(String reportDirectory) { this.reportPath = reportDirectory; } - public Set getReportFormats() { + public Collection getReportFormats() { return reportFormats; } diff --git a/HeadlessJavaScanner/src/test/java/scanner/targets/ReportFormatTest.java b/HeadlessJavaScanner/src/test/java/scanner/targets/ReportFormatTest.java index 022826fc2..22b85db68 100644 --- a/HeadlessJavaScanner/src/test/java/scanner/targets/ReportFormatTest.java +++ b/HeadlessJavaScanner/src/test/java/scanner/targets/ReportFormatTest.java @@ -23,6 +23,7 @@ public class ReportFormatTest extends AbstractHeadlessTest { private static final String csvSummaryReportPath = rootPath + "CryptoAnalysis-Report-Summary.csv"; private static final String sarifReportPath = rootPath + "CryptoAnalysis-Report.json"; + private static final String visualizationPath = rootPath + "visualization.png"; @Before public void setup() { @@ -152,6 +153,24 @@ public void testMultipleFormatsCreation() { Assert.assertTrue(sarifReport.exists()); } + @Test + public void testVisualization() { + File vizFile = new File(visualizationPath); + if (vizFile.exists()) { + vizFile.delete(); + } + String mavenProjectPath = + new File("../CryptoAnalysisTargets/ReportFormatExample").getAbsolutePath(); + MavenProject mavenProject = createAndCompile(mavenProjectPath); + + HeadlessJavaScanner scanner = createScanner(mavenProject); + scanner.setReportDirectory(outputDir.getAbsolutePath()); + scanner.setVisualization(true); + scanner.run(); + + Assert.assertTrue(vizFile.exists()); + } + @After public void tearDown() { try { diff --git a/HeadlessJavaScanner/src/test/java/settings/CommandLineTest.java b/HeadlessJavaScanner/src/test/java/settings/CommandLineTest.java index ce468d02e..6ac4294e0 100644 --- a/HeadlessJavaScanner/src/test/java/settings/CommandLineTest.java +++ b/HeadlessJavaScanner/src/test/java/settings/CommandLineTest.java @@ -253,6 +253,23 @@ public void testInvalidReportFormat() { @Test public void testVisualization() { + String reportPath = "path/to/report"; + String[] args = + new String[] { + APP_PATH, + EXAMPLE_APP_PATH, + RULES_DIR, + EXAMPLE_RULES_DIR, + VISUALIZATION, + REPORT_PATH, + reportPath + }; + HeadlessJavaScanner scanner = HeadlessJavaScanner.createFromCLISettings(args); + Assert.assertTrue(scanner.isVisualization()); + } + + @Test(expected = CryptoAnalysisParserException.class) + public void testInvalidVisualization() { String[] args = new String[] { APP_PATH, EXAMPLE_APP_PATH, RULES_DIR, EXAMPLE_RULES_DIR, VISUALIZATION From 96d05f392419d57a48f44a4fae10da9291825972 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Tue, 17 Dec 2024 17:42:01 +0100 Subject: [PATCH 21/32] Add job to synchronize master and develop after deployment --- .github/workflows/deploy_and_release.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/deploy_and_release.yml b/.github/workflows/deploy_and_release.yml index a3b8731db..83979b5e0 100644 --- a/.github/workflows/deploy_and_release.yml +++ b/.github/workflows/deploy_and_release.yml @@ -31,6 +31,21 @@ jobs: OSSRH_USERNAME: ${{ secrets.SONATYPE_USER }} OSSRH_PASSWORD: ${{ secrets.SONATYPE_PW }} + synchronize: + runs-on: ubuntu-latest + needs: deployment + steps: + - name: Checkout source code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Synchronize master and develop + run: | + gh pr create -B develop -H master -t "Synchronize version in master and develop" -b "Update the version in `develop` from `master`" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + release: runs-on: ubuntu-latest needs: deployment From 8f9d64104262b7dc5a2d9175a5ab81631e7ae42b Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Wed, 18 Dec 2024 11:13:57 +0100 Subject: [PATCH 22/32] Fix ensuring negated predicates --- .../AnalysisSeedWithSpecification.java | 13 +- .../test/UsagePatternTestingFramework.java | 1 + .../HasEnsuredPredicateAssertion.java | 9 +- .../HasGeneratedPredicateAssertion.java | 7 +- .../HasNotGeneratedPredicateAssertion.java | 4 +- .../NotHasEnsuredPredicateAssertion.java | 6 +- .../alternatives/AlternativesTest.java | 129 +++++++++++++++ .../error/predicate/alternatives/Source.java | 10 ++ .../error/predicate/alternatives/Target.java | 12 ++ .../RequiredPredicatesTest.java | 147 +++++++++++------- .../test/java/tests/jca/SecretKeyTest.java | 4 +- .../testrules/alternatives/Source.crysl | 13 ++ .../testrules/alternatives/Target.crysl | 17 ++ 13 files changed, 307 insertions(+), 65 deletions(-) create mode 100644 CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/AlternativesTest.java create mode 100644 CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/Source.java create mode 100644 CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/Target.java create mode 100644 CryptoAnalysis/src/test/resources/testrules/alternatives/Source.crysl create mode 100644 CryptoAnalysis/src/test/resources/testrules/alternatives/Target.crysl diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java b/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java index 1db361d42..ef589077e 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java @@ -302,7 +302,13 @@ private boolean isMethodToAcceptingState(DeclaredMethod method) { @Override public void expectPredicate( Statement statement, CrySLPredicate predicate, IAnalysisSeed seed, int paramIndex) { - expectedPredicates.put(statement, new ExpectedPredicateOnSeed(predicate, seed, paramIndex)); + CrySLPredicate expectedPred; + if (predicate.isNegated()) { + expectedPred = predicate.invertNegation(); + } else { + expectedPred = predicate; + } + expectedPredicates.put(statement, new ExpectedPredicateOnSeed(expectedPred, seed, paramIndex)); } public void addRequiringSeed(AnalysisSeedWithSpecification seed) { @@ -550,6 +556,7 @@ public void ensurePredicates() { Collection expectedPredStatements = expectedPredicates.keySet(); + // TODO Check for relevant predicates? Collection predsToBeEnsured = new HashSet<>(specification.getPredicates()); for (AbstractPredicate predicate : indirectlyEnsuredPredicates) { predsToBeEnsured.add(predicate.getPredicate().toNormalCrySLPredicate()); @@ -572,8 +579,8 @@ && isPredConditionViolated(predToBeEnsured)) { } for (Statement statement : expectedPredStatements) { - if (!expectedPredicatesAtStatement(statement) - .contains(predToBeEnsured.toNormalCrySLPredicate())) { + Collection expectedPreds = expectedPredicatesAtStatement(statement); + if (!expectedPreds.contains(predToBeEnsured.toNormalCrySLPredicate())) { continue; } diff --git a/CryptoAnalysis/src/test/java/test/UsagePatternTestingFramework.java b/CryptoAnalysis/src/test/java/test/UsagePatternTestingFramework.java index 6b7426552..2f6b1c0dd 100644 --- a/CryptoAnalysis/src/test/java/test/UsagePatternTestingFramework.java +++ b/CryptoAnalysis/src/test/java/test/UsagePatternTestingFramework.java @@ -95,6 +95,7 @@ protected DataFlowScope createDataFlowScope() { protected SceneTransformer createAnalysisTransformer() throws ImprecisionException { Options.v().setPhaseOption("jb", "use-original-names:false"); + Options.v().set_keep_line_number(true); return new SceneTransformer() { diff --git a/CryptoAnalysis/src/test/java/test/assertions/HasEnsuredPredicateAssertion.java b/CryptoAnalysis/src/test/java/test/assertions/HasEnsuredPredicateAssertion.java index 1487a1073..b2240fe61 100644 --- a/CryptoAnalysis/src/test/java/test/assertions/HasEnsuredPredicateAssertion.java +++ b/CryptoAnalysis/src/test/java/test/assertions/HasEnsuredPredicateAssertion.java @@ -53,7 +53,14 @@ public String toString() { if (predName == null) { return "Expected a predicate for " + val.getVariableName() + " @ " + stmt; } else { - return "Expected '" + predName + "' ensured on " + val.getVariableName() + " @ " + stmt; + return "Expected '" + + predName + + "' ensured on " + + val.getVariableName() + + " @ " + + stmt + + " @ line " + + stmt.getStartLineNumber(); } } } diff --git a/CryptoAnalysis/src/test/java/test/assertions/HasGeneratedPredicateAssertion.java b/CryptoAnalysis/src/test/java/test/assertions/HasGeneratedPredicateAssertion.java index 824b90e05..b842f60f2 100644 --- a/CryptoAnalysis/src/test/java/test/assertions/HasGeneratedPredicateAssertion.java +++ b/CryptoAnalysis/src/test/java/test/assertions/HasGeneratedPredicateAssertion.java @@ -40,6 +40,11 @@ public Statement getStatement() { @Override public String toString() { - return "Expected " + val.getVariableName() + " to generate a predicate @ " + statement; + return "Expected " + + val.getVariableName() + + " to generate a predicate @ " + + statement + + " @ line " + + statement.getStartLineNumber(); } } diff --git a/CryptoAnalysis/src/test/java/test/assertions/HasNotGeneratedPredicateAssertion.java b/CryptoAnalysis/src/test/java/test/assertions/HasNotGeneratedPredicateAssertion.java index 5ee66eed2..88cf79668 100644 --- a/CryptoAnalysis/src/test/java/test/assertions/HasNotGeneratedPredicateAssertion.java +++ b/CryptoAnalysis/src/test/java/test/assertions/HasNotGeneratedPredicateAssertion.java @@ -43,6 +43,8 @@ public String toString() { return "Did not expected " + val.getVariableName() + " to generate a predicate @ " - + statement; + + statement + + " @ line " + + statement.getStartLineNumber(); } } diff --git a/CryptoAnalysis/src/test/java/test/assertions/NotHasEnsuredPredicateAssertion.java b/CryptoAnalysis/src/test/java/test/assertions/NotHasEnsuredPredicateAssertion.java index 6bce7e224..6dc5d04bd 100644 --- a/CryptoAnalysis/src/test/java/test/assertions/NotHasEnsuredPredicateAssertion.java +++ b/CryptoAnalysis/src/test/java/test/assertions/NotHasEnsuredPredicateAssertion.java @@ -51,14 +51,16 @@ public void reported(Collection seed, AbstractPredicate pred) { @Override public String toString() { if (predName == null) { - return "Did not expect a predicate for " + val.getVariableName() + " @ " + stmt; + return "Did not expect a predicate for " + val.getVariableName() + " @ " + stmt + " @ line " + stmt.getStartLineNumber(); } else { return "Did not expect '" + predName + "' ensured on " + val.getVariableName() + " @ " - + stmt; + + stmt + + " @ line " + + stmt.getStartLineNumber(); } } } diff --git a/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/AlternativesTest.java b/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/AlternativesTest.java new file mode 100644 index 000000000..41b7abc6a --- /dev/null +++ b/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/AlternativesTest.java @@ -0,0 +1,129 @@ +package tests.error.predicate.alternatives; + +import org.junit.Test; +import test.TestConstants; +import test.UsagePatternTestingFramework; +import test.assertions.Assertions; + +public class AlternativesTest extends UsagePatternTestingFramework { + + @Override + protected String getRulesetPath() { + return TestConstants.RULES_TEST_DIR + "alternatives"; + } + + @Test + public void testPositiveSingleAlternative() { + Source source1 = new Source(); + source1.ensurePredOnThis(); + Assertions.hasEnsuredPredicate(source1); + + Target target1 = new Target(); + target1.requireSource(source1); + Assertions.hasGeneratedPredicate(target1); + + String string = "test"; + Source source2 = new Source(); + source2.ensurePredOnString(string); + Assertions.hasGeneratedPredicate(source2); + + Target target2 = new Target(); + target2.requireString(string); + Assertions.hasGeneratedPredicate(target2); + + Assertions.predicateErrors(0); + } + + @Test + public void testNegativeSingleAlternative() { + Source source1 = new Source(); + Assertions.notHasEnsuredPredicate(source1); + + Target target1 = new Target(); + target1.requireSource(source1); + Assertions.hasNotGeneratedPredicate(target1); + + String string = "test"; + Source source2 = new Source(); + Assertions.hasNotGeneratedPredicate(source2); + + Target target2 = new Target(); + target2.requireString(string); + Assertions.hasNotGeneratedPredicate(target2); + + Assertions.predicateErrors(0); + } + + @Test + public void testAlternativeWithoutStatement() { + Source source = new Source(); + Assertions.notHasEnsuredPredicate(source); + + Target target = new Target(); + target.requireSource(source); + target.requireString("test"); + + // Alternatives belong to the same predicate -> one error + Assertions.predicateErrors(1); + } + + @Test + public void testAlternativeWithSingleStatement() { + Source source1 = new Source(); + source1.ensurePredOnThis(); + Assertions.hasEnsuredPredicate(source1); + + Target target1 = new Target(); + target1.requireSource(source1); + target1.requireString("test"); + + String string = "test"; + Source source2 = new Source(); + source2.ensurePredOnString(string); + Assertions.hasEnsuredPredicate(source2); + + Target target2 = new Target(); + target2.requireSource(source2); + target2.requireString(string); + + Assertions.predicateErrors(0); + } + + @Test + public void testAlternativeWithMultipleStatements() { + String string = "test"; + Source source = new Source(); + source.ensurePredOnThis(); + source.ensurePredOnString(string); + Assertions.hasEnsuredPredicate(source); + + Target target = new Target(); + target.requireSource(source); + target.requireString(string); + + Assertions.predicateErrors(0); + } + + @Test + public void testNegativeAlternativesAtSameStatement() { + Source source = new Source(); + Assertions.notHasEnsuredPredicate(source); + + Target target = new Target(); + target.requireSourceAndString(source, "test"); + + Assertions.predicateErrors(1); + } + + @Test + public void testPositiveAlternativesAtSameStatement() { + Source source1 = new Source(); + source1.ensurePredOnThis(); + Assertions.hasEnsuredPredicate(source1); + + Target target1 = new Target(); + target1.requireSourceAndString(source1, "test"); + + Assertions.predicateErrors(0); + } +} diff --git a/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/Source.java b/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/Source.java new file mode 100644 index 000000000..a21e44303 --- /dev/null +++ b/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/Source.java @@ -0,0 +1,10 @@ +package tests.error.predicate.alternatives; + +public class Source { + + public Source() {} + + public void ensurePredOnThis() {} + + public void ensurePredOnString(String s) {} +} diff --git a/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/Target.java b/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/Target.java new file mode 100644 index 000000000..15e7905c1 --- /dev/null +++ b/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/Target.java @@ -0,0 +1,12 @@ +package tests.error.predicate.alternatives; + +public class Target { + + public Target() {} + + public void requireSource(Source source) {} + + public void requireString(String string) {} + + public void requireSourceAndString(Source source, String string) {} +} diff --git a/CryptoAnalysis/src/test/java/tests/error/predicate/requiredpredicate/RequiredPredicatesTest.java b/CryptoAnalysis/src/test/java/tests/error/predicate/requiredpredicate/RequiredPredicatesTest.java index b4da5ed44..8e5498e90 100644 --- a/CryptoAnalysis/src/test/java/tests/error/predicate/requiredpredicate/RequiredPredicatesTest.java +++ b/CryptoAnalysis/src/test/java/tests/error/predicate/requiredpredicate/RequiredPredicatesTest.java @@ -28,10 +28,12 @@ public void pred1OnPos1() { A noPred1OnA = new A(); Assertions.notHasEnsuredPredicate(noPred1OnA); + // correct Requires r1 = new Requires(); r1.pred1onPos1(pred1OnA); Assertions.hasGeneratedPredicate(r1); + // 1 required error Requires r2 = new Requires(); r2.pred1onPos1(noPred1OnA); Assertions.hasNotGeneratedPredicate(r2); @@ -39,7 +41,6 @@ public void pred1OnPos1() { Assertions.predicateErrors(1); } - @Ignore("Predicate that are not checked yet are assumed to be true") @Test public void notPred1onPos1() { A pred1OnA = new A(); @@ -49,15 +50,17 @@ public void notPred1onPos1() { A noPred1OnA = new A(); Assertions.notHasEnsuredPredicate(noPred1OnA); + // correct Requires r1 = new Requires(); r1.notPred1onPos1(noPred1OnA); Assertions.hasGeneratedPredicate(r1); + // 1 contradiction error Requires r2 = new Requires(); r2.notPred1onPos1(pred1OnA); Assertions.hasNotGeneratedPredicate(r2); - Assertions.predicateErrors(1); + Assertions.predicateContradictionErrors(1); } // AND @@ -72,18 +75,22 @@ public void pred1onPos1_AND_pred1onPos2() { A noPred1onA = new A(); Assertions.notHasEnsuredPredicate(noPred1onA); + // correct Requires r1 = new Requires(); r1.pred1onPos1_AND_pred1onPos2(pred1onA, pred1onA); Assertions.hasGeneratedPredicate(r1); + // 1 required error Requires r2 = new Requires(); r2.pred1onPos1_AND_pred1onPos2(pred1onA, noPred1onA); Assertions.hasNotGeneratedPredicate(r2); + // 1 required error Requires r3 = new Requires(); r3.pred1onPos1_AND_pred1onPos2(noPred1onA, pred1onA); Assertions.hasNotGeneratedPredicate(r3); + // 1 required + 1 required error Requires r4 = new Requires(); r4.pred1onPos1_AND_pred1onPos2(noPred1onA, noPred1onA); Assertions.hasNotGeneratedPredicate(r4); @@ -91,19 +98,6 @@ public void pred1onPos1_AND_pred1onPos2() { Assertions.predicateErrors(4); } - @Test - public void test() { - A noPred1onA = new A(); - Assertions.notHasEnsuredPredicate(noPred1onA); - - Requires r4 = new Requires(); - r4.pred1onPos1_AND_pred1onPos2(noPred1onA, noPred1onA); - Assertions.notHasEnsuredPredicate(r4); - - Assertions.predicateErrors(2); - } - - @Ignore @Test public void pred1onPos1_AND_notPred1onPos2() { A pred1onA = new A(); @@ -113,26 +107,30 @@ public void pred1onPos1_AND_notPred1onPos2() { A noPredOnA = new A(); Assertions.notHasEnsuredPredicate(noPredOnA); + // correct Requires r1 = new Requires(); r1.pred1onPos1_AND_notPred1onPos2(pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); + // 1 required + 1 contradiction error Requires r2 = new Requires(); r2.pred1onPos1_AND_notPred1onPos2(noPredOnA, pred1onA); - Assertions.notHasEnsuredPredicate(r2); + Assertions.hasNotGeneratedPredicate(r2); + // 1 contradiction error Requires r3 = new Requires(); r3.pred1onPos1_AND_notPred1onPos2(pred1onA, pred1onA); - Assertions.notHasEnsuredPredicate(r3); + Assertions.hasNotGeneratedPredicate(r3); + // 1 required error Requires r4 = new Requires(); r4.pred1onPos1_AND_notPred1onPos2(noPredOnA, noPredOnA); - Assertions.notHasEnsuredPredicate(r4); + Assertions.hasNotGeneratedPredicate(r4); - Assertions.predicateErrors(4); + Assertions.predicateErrors(2); + Assertions.predicateContradictionErrors(2); } - @Ignore @Test public void notPred1onPos1_AND_pred1onPos2() { A pred1onA = new A(); @@ -142,26 +140,30 @@ public void notPred1onPos1_AND_pred1onPos2() { A noPredOnA = new A(); Assertions.notHasEnsuredPredicate(noPredOnA); + // correct Requires r1 = new Requires(); r1.notPred1onPos1_AND_pred1onPos2(noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); + // 1 contradiction + 1 required error Requires r2 = new Requires(); r2.notPred1onPos1_AND_pred1onPos2(pred1onA, noPredOnA); - Assertions.notHasEnsuredPredicate(r2); + Assertions.hasNotGeneratedPredicate(r2); + // 1 contradiction error Requires r3 = new Requires(); r3.notPred1onPos1_AND_pred1onPos2(pred1onA, pred1onA); - Assertions.notHasEnsuredPredicate(r3); + Assertions.hasNotGeneratedPredicate(r3); + // 1 required error Requires r4 = new Requires(); r4.notPred1onPos1_AND_pred1onPos2(noPredOnA, noPredOnA); - Assertions.notHasEnsuredPredicate(r4); + Assertions.hasNotGeneratedPredicate(r4); - Assertions.predicateErrors(4); + Assertions.predicateErrors(2); + Assertions.predicateContradictionErrors(2); } - @Ignore @Test public void notPred1onPos1_AND_notPred1onPos2() { A pred1onA = new A(); @@ -171,23 +173,28 @@ public void notPred1onPos1_AND_notPred1onPos2() { A noPredOnA = new A(); Assertions.notHasEnsuredPredicate(noPredOnA); + // correct Requires r1 = new Requires(); r1.notPred1onPos1_AND_notPred1onPos2(noPredOnA, noPredOnA); Assertions.hasGeneratedPredicate(r1); + // 1 contradiction error Requires r2 = new Requires(); r2.notPred1onPos1_AND_notPred1onPos2(pred1onA, noPredOnA); Assertions.hasNotGeneratedPredicate(r2); + // 1 contradiction error Requires r3 = new Requires(); - r3.notPred1onPos1_AND_notPred1onPos2(pred1onA, pred1onA); + r3.notPred1onPos1_AND_notPred1onPos2(noPredOnA, pred1onA); Assertions.hasNotGeneratedPredicate(r3); + // 2 contradiction error Requires r4 = new Requires(); - r4.notPred1onPos1_AND_notPred1onPos2(noPredOnA, pred1onA); + r4.notPred1onPos1_AND_notPred1onPos2(pred1onA, pred1onA); Assertions.hasNotGeneratedPredicate(r4); - Assertions.predicateErrors(3); + Assertions.predicateErrors(0); + Assertions.predicateContradictionErrors(4); } // multi predicates @@ -204,26 +211,29 @@ public void pred1onPos1_AND_pred2onPos2() { A noPredOnA = new A(); Assertions.notHasEnsuredPredicate(noPredOnA); + // correct Requires r1 = new Requires(); r1.pred1onPos1_AND_pred2onPos2(pred1onA, pred2onA); Assertions.hasGeneratedPredicate(r1); + // 1 required error Requires r2 = new Requires(); r2.pred1onPos1_AND_pred2onPos2(pred1onA, noPredOnA); Assertions.hasNotGeneratedPredicate(r2); + // 1 required error Requires r3 = new Requires(); - r3.pred1onPos1_AND_pred2onPos2(noPredOnA, noPredOnA); + r3.pred1onPos1_AND_pred2onPos2(noPredOnA, pred2onA); Assertions.hasNotGeneratedPredicate(r3); + // 1 required + 1 required error Requires r4 = new Requires(); - r4.pred1onPos1_AND_pred2onPos2(noPredOnA, pred2onA); + r4.pred1onPos1_AND_pred2onPos2(noPredOnA, noPredOnA); Assertions.hasNotGeneratedPredicate(r4); Assertions.predicateErrors(4); } - @Ignore("Predicate that are not checked yet are assumed to be true") @Test public void pred1onPos1_AND_notPred2onPos2() { A pred1onA = new A(); @@ -237,26 +247,30 @@ public void pred1onPos1_AND_notPred2onPos2() { A noPredOnA = new A(); Assertions.notHasEnsuredPredicate(noPredOnA); + // correct Requires r1 = new Requires(); r1.pred1onPos1_AND_notPred2onPos2(pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); + // 1 required + 1 contradiction error Requires r2 = new Requires(); r2.pred1onPos1_AND_notPred2onPos2(noPredOnA, pred2onA); - Assertions.notHasEnsuredPredicate(r2); + Assertions.hasNotGeneratedPredicate(r2); + // 1 contradiction error Requires r3 = new Requires(); r3.pred1onPos1_AND_notPred2onPos2(pred1onA, pred2onA); - Assertions.notHasEnsuredPredicate(r3); + Assertions.hasNotGeneratedPredicate(r3); + // 1 required error Requires r4 = new Requires(); r4.pred1onPos1_AND_notPred2onPos2(noPredOnA, noPredOnA); - Assertions.notHasEnsuredPredicate(r4); + Assertions.hasNotGeneratedPredicate(r4); - Assertions.predicateErrors(4); + Assertions.predicateErrors(2); + Assertions.predicateContradictionErrors(2); } - @Ignore("Predicate that are not checked yet are assumed to be true") @Test public void notPred1onPos1_AND_pred2onPos2() { A pred1onA = new A(); @@ -270,26 +284,30 @@ public void notPred1onPos1_AND_pred2onPos2() { A noPredOnA = new A(); Assertions.notHasEnsuredPredicate(noPredOnA); + // correct Requires r1 = new Requires(); r1.notPred1onPos1_AND_pred2onPos2(noPredOnA, pred2onA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); + // 1 contradiction + 1 required error Requires r2 = new Requires(); r2.notPred1onPos1_AND_pred2onPos2(pred1onA, noPredOnA); - Assertions.notHasEnsuredPredicate(r2); + Assertions.hasNotGeneratedPredicate(r2); + // 1 contradiction Requires r3 = new Requires(); r3.notPred1onPos1_AND_pred2onPos2(pred1onA, pred2onA); - Assertions.notHasEnsuredPredicate(r3); + Assertions.hasNotGeneratedPredicate(r3); + // 1 required error Requires r4 = new Requires(); r4.notPred1onPos1_AND_pred2onPos2(noPredOnA, noPredOnA); Assertions.notHasEnsuredPredicate(r4); - Assertions.predicateErrors(4); + Assertions.predicateErrors(2); + Assertions.predicateContradictionErrors(2); } - @Ignore("Predicate that are not checked yet are assumed to be true") @Test public void notPred1onPos1_AND_notPred2onPos2() { A pred1onA = new A(); @@ -303,23 +321,28 @@ public void notPred1onPos1_AND_notPred2onPos2() { A noPredOnA = new A(); Assertions.notHasEnsuredPredicate(noPredOnA); + // correct Requires r1 = new Requires(); r1.notPred1onPos1_AND_notPred2onPos2(noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); + // 1 contradiction error Requires r2 = new Requires(); r2.notPred1onPos1_AND_notPred2onPos2(pred1onA, noPredOnA); - Assertions.notHasEnsuredPredicate(r2); + Assertions.hasNotGeneratedPredicate(r2); + // 1 contradiction error Requires r3 = new Requires(); - r3.notPred1onPos1_AND_notPred2onPos2(pred1onA, pred2onA); - Assertions.notHasEnsuredPredicate(r3); + r3.notPred1onPos1_AND_notPred2onPos2(noPredOnA, pred2onA); + Assertions.hasNotGeneratedPredicate(r3); + // 1 contradiction + 1 contradiction error Requires r4 = new Requires(); - r4.notPred1onPos1_AND_notPred2onPos2(noPredOnA, pred2onA); - Assertions.notHasEnsuredPredicate(r4); + r4.notPred1onPos1_AND_notPred2onPos2(pred1onA, pred2onA); + Assertions.hasNotGeneratedPredicate(r4); - Assertions.predicateErrors(4); + Assertions.predicateErrors(0); + Assertions.predicateContradictionErrors(4); } // OR @@ -339,24 +362,38 @@ public void pred1onPos1_OR_pred1onPos2() { // assert true Requires r1 = new Requires(); r1.pred1onPos1_OR_pred1onPos2(pred1onA, pred1onA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); Requires r2 = new Requires(); r2.pred1onPos1_OR_pred1onPos2(pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r2); + Assertions.hasGeneratedPredicate(r2); Requires r3 = new Requires(); r3.pred1onPos1_OR_pred1onPos2(noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r3); + Assertions.hasGeneratedPredicate(r3); // assert false Requires r4 = new Requires(); r4.pred1onPos1_OR_pred1onPos2(noPredOnA, noPredOnA); - Assertions.notHasEnsuredPredicate(r4); + Assertions.hasNotGeneratedPredicate(r4); Assertions.predicateErrors(2); // two, because each parameter will be reported } + @Test + public void test() { + A pred1onA = new A(); + pred1onA.ensurePred1onThis(); + Assertions.hasEnsuredPredicate(pred1onA); + + A noPredOnA = new A(); + Assertions.notHasEnsuredPredicate(noPredOnA); + + Requires r2 = new Requires(); + r2.pred1onPos1_OR_pred1onPos2(pred1onA, noPredOnA); + Assertions.hasGeneratedPredicate(r2); + } + @Ignore @Test public void pred1onPos1_OR_notPred1onPos2() { diff --git a/CryptoAnalysis/src/test/java/tests/jca/SecretKeyTest.java b/CryptoAnalysis/src/test/java/tests/jca/SecretKeyTest.java index 0c3de2c36..766a593a6 100644 --- a/CryptoAnalysis/src/test/java/tests/jca/SecretKeyTest.java +++ b/CryptoAnalysis/src/test/java/tests/jca/SecretKeyTest.java @@ -33,9 +33,9 @@ protected String getRulesetPath() { public void test() throws GeneralSecurityException { KeyGenerator generator = KeyGenerator.getInstance("AES"); SecretKey key = generator.generateKey(); - byte[] bytes = key.getEncoded(); - SecretKeySpec spec = new SecretKeySpec(bytes, "AES"); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, key); } @Test diff --git a/CryptoAnalysis/src/test/resources/testrules/alternatives/Source.crysl b/CryptoAnalysis/src/test/resources/testrules/alternatives/Source.crysl new file mode 100644 index 000000000..373bc32bc --- /dev/null +++ b/CryptoAnalysis/src/test/resources/testrules/alternatives/Source.crysl @@ -0,0 +1,13 @@ +SPEC tests.error.predicate.alternatives.Source + +OBJECTS + java.lang.String string; + +EVENTS + Con: Source(); + EnsOnThis: ensurePredOnThis(); + EnsOnString: ensurePredOnString(string); + +ENSURES + predSource[this] after EnsOnThis; + predString[string]; diff --git a/CryptoAnalysis/src/test/resources/testrules/alternatives/Target.crysl b/CryptoAnalysis/src/test/resources/testrules/alternatives/Target.crysl new file mode 100644 index 000000000..edb8baea6 --- /dev/null +++ b/CryptoAnalysis/src/test/resources/testrules/alternatives/Target.crysl @@ -0,0 +1,17 @@ +SPEC tests.error.predicate.alternatives.Target + +OBJECTS + tests.error.predicate.alternatives.Source source; + java.lang.String string; + +EVENTS + Con: Target(); + ReqSource: requireSource(source); + ReqString: requireString(string); + ReqSourceAndString: requireSourceAndString(source, string); + +REQUIRES + predSource[source] || predString[string]; + +ENSURES + generatedTarget[this]; From 20a6a1c730b29299da194f579040c3e809952547 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Thu, 19 Dec 2024 13:07:30 +0100 Subject: [PATCH 23/32] Fix logic for alternative predicates --- .../analysis/AlternativeReqPredicate.java | 95 +++--- .../AnalysisSeedWithSpecification.java | 219 ++++++++------ .../crypto/analysis/PredicateHandler.java | 49 ++-- .../analysis/RequiredCrySLPredicate.java | 11 + .../AbstractRequiredPredicateError.java | 58 ++++ .../errors/AlternativeReqPredicateError.java | 97 +++++++ .../errors/RequiredPredicateError.java | 78 ++--- .../crypto/constraints/ConstraintSolver.java | 87 +++--- .../crypto/listener/AnalysisReporter.java | 3 + .../java/crypto/listener/ErrorCollector.java | 6 + .../java/crypto/listener/IErrorListener.java | 3 + .../java/test/UsagePatternErrorListener.java | 11 + .../NotHasEnsuredPredicateAssertion.java | 7 +- .../alternatives/AlternativesTest.java | 129 --------- .../error/predicate/alternatives/Source.java | 10 - .../error/predicate/alternatives/Target.java | 12 - .../RequiredPredicatesTest.java | 272 +++++++++--------- .../testrules/alternatives/Source.crysl | 13 - .../testrules/alternatives/Target.crysl | 17 -- .../targets/BouncyCastleHeadlessTest.java | 68 +++-- .../targets/BragaCryptoGoodUsesTest.java | 79 +++-- .../targets/BragaCryptoMisusesTest.java | 166 +++++++---- .../targets/CogniCryptGeneratedCodeTest.java | 6 +- .../java/scanner/targets/CryptoGuardTest.java | 21 +- .../scanner/targets/IgnoreSectionsTest.java | 7 +- .../scanner/targets/MUBenchExamplesTest.java | 4 +- .../scanner/targets/ReportedIssueTest.java | 16 +- .../targets/SootJava9ConfigurationTest.java | 4 +- .../targets/StaticAnalysisDemoTest.java | 9 +- 29 files changed, 855 insertions(+), 702 deletions(-) create mode 100644 CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiredPredicateError.java create mode 100644 CryptoAnalysis/src/main/java/crypto/analysis/errors/AlternativeReqPredicateError.java delete mode 100644 CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/AlternativesTest.java delete mode 100644 CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/Source.java delete mode 100644 CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/Target.java delete mode 100644 CryptoAnalysis/src/test/resources/testrules/alternatives/Source.crysl delete mode 100644 CryptoAnalysis/src/test/resources/testrules/alternatives/Target.crysl diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/AlternativeReqPredicate.java b/CryptoAnalysis/src/main/java/crypto/analysis/AlternativeReqPredicate.java index a15a9c7a6..222f1e505 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/AlternativeReqPredicate.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/AlternativeReqPredicate.java @@ -4,76 +4,89 @@ import crysl.rule.CrySLPredicate; import crysl.rule.ISLConstraint; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; +/** + * Wrapper class for required predicates with alternatives. Predicates from the REQUIRES section may + * have the following form: + * + *
{@code
+ * REQUIRES
+ *    generatedKey[...] || generatedPubKey[...] || generatedPrivKey[...];
+ * }
+ * + * According to the CrySL specification, "generatedKey" is the base predicate that determines the + * statement where the predicates should be ensured. If an alternative refers to some other + * statement, it is ignored for this predicate. + */ public class AlternativeReqPredicate implements ISLConstraint { - private final List alternatives; - private final Statement stmt; - private final int paramIndex; + private final RequiredCrySLPredicate basePredicate; + private final Collection allAlternatives; + private final Collection relAlternatives; - public AlternativeReqPredicate(CrySLPredicate alternativeOne, Statement stmt, int paramIndex) { - this.alternatives = new ArrayList<>(); - this.alternatives.add(alternativeOne); - this.stmt = stmt; - this.paramIndex = paramIndex; + public AlternativeReqPredicate( + RequiredCrySLPredicate basePredicate, + Collection allAlternatives, + Collection relAlternatives) { + this.basePredicate = basePredicate; + this.allAlternatives = List.copyOf(allAlternatives); + this.relAlternatives = Set.copyOf(relAlternatives); } - @Override - public int hashCode() { - return Objects.hash(alternatives, stmt, paramIndex); + public Collection getAllAlternatives() { + return allAlternatives; } - @Override - public boolean equals(Object obj) { - return obj instanceof AlternativeReqPredicate other - && Objects.equals(alternatives, other.alternatives) - && Objects.equals(stmt, other.stmt) - && paramIndex == other.paramIndex; + public Collection getRelAlternatives() { + return relAlternatives; } public Statement getLocation() { - return stmt; - } - - public int getParamIndex() { - return paramIndex; + return basePredicate.getLocation(); } @Override - public String toString() { - return alternatives.stream() - .map(CrySLPredicate::toString) - .collect(Collectors.joining(" OR ")) - + " @ " - + stmt - + " @ index " - + paramIndex; + public List getInvolvedVarNames() { + List involvedVarNames = new ArrayList<>(); + for (CrySLPredicate alt : allAlternatives) { + involvedVarNames.addAll(alt.getInvolvedVarNames()); + } + return involvedVarNames; } @Override public String getName() { - return alternatives.stream() + return allAlternatives.stream() .map(CrySLPredicate::getName) .collect(Collectors.joining(" OR ")); } @Override - public List getInvolvedVarNames() { - List involvedVarNames = new ArrayList<>(); - for (CrySLPredicate alt : alternatives) { - involvedVarNames.addAll(alt.getInvolvedVarNames()); - } - return involvedVarNames; + public int hashCode() { + return Objects.hash(allAlternatives, relAlternatives); } - public List getAlternatives() { - return alternatives; + @Override + public boolean equals(Object obj) { + return obj instanceof AlternativeReqPredicate other + && Objects.equals(allAlternatives, other.getAllAlternatives()) + && Objects.equals(relAlternatives, other.getRelAlternatives()); } - public void addAlternative(CrySLPredicate newAlt) { - alternatives.add(newAlt); + @Override + public String toString() { + return allAlternatives.stream() + .map(CrySLPredicate::toString) + .collect(Collectors.joining(" OR ")) + + " (Rel: " + + relAlternatives.stream() + .map(e -> e.getPred().toString()) + .collect(Collectors.joining(", ")) + + ")"; } } diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java b/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java index ef589077e..9bed5bb99 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java @@ -38,7 +38,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -308,7 +307,8 @@ public void expectPredicate( } else { expectedPred = predicate; } - expectedPredicates.put(statement, new ExpectedPredicateOnSeed(expectedPred, seed, paramIndex)); + expectedPredicates.put( + statement, new ExpectedPredicateOnSeed(expectedPred, seed, paramIndex)); } public void addRequiringSeed(AnalysisSeedWithSpecification seed) { @@ -332,11 +332,12 @@ private void initializeDependantSeedsFromRequiredPredicates(Collection(reqPred.getPred(), reqPred.getParamIndex()); reqPreds.put(reqPred.getLocation(), entry); } else if (constraint instanceof AlternativeReqPredicate altPred) { - for (CrySLPredicate predicate : altPred.getAlternatives()) { - Map.Entry entry = - new AbstractMap.SimpleEntry<>(predicate, altPred.getParamIndex()); + for (RequiredCrySLPredicate reqPred : altPred.getRelAlternatives()) { + CrySLPredicate predicate = reqPred.getPred(); - reqPreds.put(altPred.getLocation(), entry); + Map.Entry entry = + new AbstractMap.SimpleEntry<>(predicate, reqPred.getParamIndex()); + reqPreds.put(reqPred.getLocation(), entry); } } } @@ -853,87 +854,126 @@ private boolean isConstraintSystemSatisfied() { * @return remainingPredicates predicates that are not satisfied */ public Collection computeMissingPredicates() { - Collection requiredPredicates = constraintSolver.getRequiredPredicates(); - Collection remainingPredicates = new HashSet<>(requiredPredicates); + Collection violatedReqPreds = computeViolatedRequiredPredicates(); + Collection violatedAltPreds = + computeViolatedAlternativePredicates(); - for (ISLConstraint pred : requiredPredicates) { - if (pred instanceof RequiredCrySLPredicate reqPred) { - // If a negated predicate is ensured, a PredicateContradictionError has to be - // reported - if (reqPred.getPred().isNegated()) { - remainingPredicates.remove(pred); - continue; + Collection result = new HashSet<>(); + result.addAll(violatedReqPreds); + result.addAll(violatedAltPreds); + + return result; + } + + public Collection computeViolatedRequiredPredicates() { + Collection requiredPredicates = new HashSet<>(); + + Collection reqPreds = constraintSolver.getRequiredPredicates(); + for (ISLConstraint constraint : reqPreds) { + if (constraint instanceof RequiredCrySLPredicate reqPred) { + requiredPredicates.add(reqPred); + } + } + + Collection remainingPredicates = new HashSet<>(requiredPredicates); + + // Remove all ensured predicates from the required predicates set -> only not ensured + // predicates are left + for (RequiredCrySLPredicate reqPred : requiredPredicates) { + // If a negated predicate is ensured, a PredicateContradictionError has to be reported + if (reqPred.getPred().isNegated()) { + remainingPredicates.remove(reqPred); + continue; + } + + // Check for basic required predicates, e.g. randomized + Collection> predsAtStatement = + ensuredPredicates.get(reqPred.getLocation()); + int reqParamIndex = reqPred.getParamIndex(); + for (Map.Entry ensPredAtIndex : predsAtStatement) { + if (doReqPredAndEnsPredMatch(reqPred.getPred(), reqParamIndex, ensPredAtIndex)) { + remainingPredicates.remove(reqPred); } + } + } - // Check for basic required predicates, e.g. randomized - Collection> predsAtStatement = - ensuredPredicates.get(reqPred.getLocation()); - int reqParamIndex = reqPred.getParamIndex(); - for (Map.Entry ensPredAtIndex : predsAtStatement) { - if (doReqPredAndEnsPredMatch( - reqPred.getPred(), reqParamIndex, ensPredAtIndex)) { - remainingPredicates.remove(pred); - } + // Check conditional required predicates + for (RequiredCrySLPredicate rem : new HashSet<>(remainingPredicates)) { + if (isPredConditionViolated(rem.getPred())) { + remainingPredicates.remove(rem); + } + } + + return remainingPredicates; + } + + public Collection computeViolatedAlternativePredicates() { + Collection alternativeReqPredicates = new HashSet<>(); + + Collection reqPreds = constraintSolver.getRequiredPredicates(); + for (ISLConstraint constraint : reqPreds) { + if (constraint instanceof AlternativeReqPredicate reqPred) { + alternativeReqPredicates.add(reqPred); + } + } + + Collection remainingPredicates = + new HashSet<>(alternativeReqPredicates); + + for (AlternativeReqPredicate altPred : alternativeReqPredicates) { + Collection positives = new HashSet<>(); + Collection negatives = new HashSet<>(); + + Collection relAlternatives = altPred.getRelAlternatives(); + for (RequiredCrySLPredicate reqPred : relAlternatives) { + CrySLPredicate predicate = reqPred.getPred(); + + if (predicate.isNegated()) { + negatives.add(reqPred); + } else { + positives.add(reqPred); } - } else if (pred instanceof AlternativeReqPredicate altPred) { - Collection alternatives = altPred.getAlternatives(); - Collection positives = - alternatives.stream().filter(e -> !e.isNegated()).toList(); - Collection negatives = - alternatives.stream().filter(CrySLPredicate::isNegated).toList(); - int altParamIndex = altPred.getParamIndex(); - - boolean satisfied = false; - Collection ensuredNegatives = - alternatives.stream() - .filter(CrySLPredicate::isNegated) - .collect(Collectors.toList()); + } + + boolean satisfied = false; + + // If any positive alternative is satisfied, the whole predicate is satisfied + for (RequiredCrySLPredicate positive : positives) { + CrySLPredicate predicate = positive.getPred(); + int paramIndex = positive.getParamIndex(); Collection> predsAtStatement = ensuredPredicates.get(altPred.getLocation()); - for (Map.Entry ensPredAtIndex : predsAtStatement) { - // If any positive alternative is satisfied, the whole predicate is satisfied - if (positives.stream() - .anyMatch( - e -> - doReqPredAndEnsPredMatch( - e, altParamIndex, ensPredAtIndex))) { + for (Map.Entry ensPred : predsAtStatement) { + if (doReqPredAndEnsPredMatch(predicate, paramIndex, ensPred)) { satisfied = true; } - - // Remove all negated alternatives that are ensured - Collection violatedNegAlternatives = - negatives.stream() - .filter( - e -> - doReqPredAndEnsPredMatch( - e, altParamIndex, ensPredAtIndex)) - .toList(); - ensuredNegatives.removeAll(violatedNegAlternatives); - } - - if (satisfied || !ensuredNegatives.isEmpty()) { - remainingPredicates.remove(pred); } } - } - // Check conditional required predicates - for (ISLConstraint rem : new HashSet<>(remainingPredicates)) { - if (rem instanceof RequiredCrySLPredicate singlePred) { - if (isPredConditionViolated(singlePred.getPred())) { - remainingPredicates.remove(singlePred); - } - } else if (rem instanceof AlternativeReqPredicate) { - Collection altPred = - ((AlternativeReqPredicate) rem).getAlternatives(); + Collection ensuredNegatives = new HashSet<>(negatives); + + // Remove all negated predicates that are ensured + for (RequiredCrySLPredicate negative : negatives) { + CrySLPredicate predicate = negative.getPred(); + int paramIndex = negative.getParamIndex(); - if (altPred.parallelStream().anyMatch(this::isPredConditionViolated)) { - remainingPredicates.remove(rem); + Collection> predsAtStatement = + ensuredPredicates.get(altPred.getLocation()); + for (Map.Entry ensPred : predsAtStatement) { + if (doReqPredAndEnsPredMatch(predicate, paramIndex, ensPred)) { + ensuredNegatives.remove(negative); + } } } + + if (satisfied || !ensuredNegatives.isEmpty()) { + remainingPredicates.remove(altPred); + } } + // TODO Check condition + return remainingPredicates; } @@ -973,8 +1013,14 @@ private boolean doReqPredAndEnsPredMatch( CrySLPredicate reqPred, int reqPredIndex, Map.Entry ensPred) { - return reqPred.equals(ensPred.getKey().getPredicate()) - && doPredsMatch(reqPred, ensPred.getKey()) + CrySLPredicate predToCheck; + if (reqPred.isNegated()) { + predToCheck = reqPred.invertNegation(); + } else { + predToCheck = reqPred; + } + return predToCheck.equals(ensPred.getKey().getPredicate()) + && doPredsMatch(predToCheck, ensPred.getKey()) && reqPredIndex == ensPred.getValue(); } @@ -1045,18 +1091,25 @@ private boolean doPredsMatch(CrySLPredicate pred, AbstractPredicate ensPred) { return requiredPredicatesExist; } - public Collection extractHiddenPredicates(RequiredCrySLPredicate predicate) { - return extractHiddenPredicates( - predicate.getLocation(), List.of(predicate.getPred()), predicate.getParamIndex()); + public Collection extractHiddenPredicates(AlternativeReqPredicate predicate) { + Collection result = new HashSet<>(); + + for (RequiredCrySLPredicate reqPred : predicate.getRelAlternatives()) { + Collection extractedPreds = extractHiddenPredicates(reqPred); + + result.addAll(extractedPreds); + } + + return result; } - public Collection extractHiddenPredicates(AlternativeReqPredicate predicate) { + public Collection extractHiddenPredicates(RequiredCrySLPredicate predicate) { return extractHiddenPredicates( - predicate.getLocation(), predicate.getAlternatives(), predicate.getParamIndex()); + predicate.getLocation(), predicate.getPred(), predicate.getParamIndex()); } private Collection extractHiddenPredicates( - Statement statement, Collection predicates, int index) { + Statement statement, CrySLPredicate predicate, int index) { Collection result = new HashSet<>(); if (!hiddenPredicates.containsKey(statement)) { @@ -1070,13 +1123,11 @@ private Collection extractHiddenPredicates( continue; } - for (CrySLPredicate pred : predicates) { - HiddenPredicate hiddenPredicate = entry.getKey(); + HiddenPredicate hiddenPredicate = entry.getKey(); - if (hiddenPredicate.getPredicate().equals(pred) - && doPredsMatch(pred, hiddenPredicate)) { - result.add(hiddenPredicate); - } + if (hiddenPredicate.getPredicate().equals(predicate) + && doPredsMatch(predicate, hiddenPredicate)) { + result.add(hiddenPredicate); } } diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/PredicateHandler.java b/CryptoAnalysis/src/main/java/crypto/analysis/PredicateHandler.java index fc4ba696d..83ad319b5 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/PredicateHandler.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/PredicateHandler.java @@ -1,9 +1,10 @@ package crypto.analysis; import crypto.analysis.errors.AbstractError; +import crypto.analysis.errors.AbstractRequiredPredicateError; +import crypto.analysis.errors.AlternativeReqPredicateError; import crypto.analysis.errors.PredicateContradictionError; import crypto.analysis.errors.RequiredPredicateError; -import crysl.rule.ISLConstraint; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -14,7 +15,7 @@ public class PredicateHandler { private final CryptoScanner cryptoScanner; - private final Map> + private final Map> requiredPredicateErrors; public PredicateHandler(CryptoScanner cryptoScanner) { @@ -63,32 +64,34 @@ private void collectContradictingPredicates() { private void collectMissingRequiredPredicates() { for (AnalysisSeedWithSpecification seed : cryptoScanner.getAnalysisSeedsWithSpec()) { requiredPredicateErrors.put(seed, new ArrayList<>()); - Collection missingPredicates = seed.computeMissingPredicates(); - - for (ISLConstraint pred : missingPredicates) { - if (pred instanceof RequiredCrySLPredicate reqPred) { - Collection hiddenPreds = seed.extractHiddenPredicates(reqPred); - - RequiredPredicateError reqPredError = - new RequiredPredicateError(seed, reqPred, hiddenPreds); - requiredPredicateErrors.get(seed).add(reqPredError); - } else if (pred instanceof AlternativeReqPredicate altReqPred) { - Collection hiddenPreds = - seed.extractHiddenPredicates(altReqPred); - - RequiredPredicateError reqPredError = - new RequiredPredicateError(seed, altReqPred, hiddenPreds); - requiredPredicateErrors.get(seed).add(reqPredError); - } + + Collection violatedReqPreds = + seed.computeViolatedRequiredPredicates(); + for (RequiredCrySLPredicate reqPred : violatedReqPreds) { + Collection hiddenPreds = seed.extractHiddenPredicates(reqPred); + + RequiredPredicateError reqPredError = + new RequiredPredicateError(seed, reqPred, hiddenPreds); + requiredPredicateErrors.get(seed).add(reqPredError); + } + + Collection violatedAltPreds = + seed.computeViolatedAlternativePredicates(); + for (AlternativeReqPredicate altPred : violatedAltPreds) { + Collection hiddenPreds = seed.extractHiddenPredicates(altPred); + + AlternativeReqPredicateError reqPredError = + new AlternativeReqPredicateError(seed, altPred, hiddenPreds); + requiredPredicateErrors.get(seed).add(reqPredError); } } } private void reportRequiredPredicateErrors() { for (AnalysisSeedWithSpecification seed : requiredPredicateErrors.keySet()) { - Collection errors = requiredPredicateErrors.get(seed); + Collection errors = requiredPredicateErrors.get(seed); - for (RequiredPredicateError reqPredError : errors) { + for (AbstractRequiredPredicateError reqPredError : errors) { seed.addError(reqPredError); cryptoScanner.getAnalysisReporter().reportError(seed, reqPredError); } @@ -97,9 +100,9 @@ private void reportRequiredPredicateErrors() { private void connectSubsequentErrors() { for (AnalysisSeedWithSpecification seed : requiredPredicateErrors.keySet()) { - Collection errors = requiredPredicateErrors.get(seed); + Collection errors = requiredPredicateErrors.get(seed); - for (RequiredPredicateError error : errors) { + for (AbstractRequiredPredicateError error : errors) { for (HiddenPredicate hiddenPredicate : error.getHiddenPredicates()) { Collection precedingErrors = hiddenPredicate.getPrecedingErrors(); diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/RequiredCrySLPredicate.java b/CryptoAnalysis/src/main/java/crypto/analysis/RequiredCrySLPredicate.java index f04a74e68..11813dade 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/RequiredCrySLPredicate.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/RequiredCrySLPredicate.java @@ -6,6 +6,17 @@ import java.util.List; import java.util.Objects; +/** + * Wrapper class for predicates from the REQUIRES section. This class only stores single predicates, + * that is, predicates of the form + * + *
{@code
+ * REQUIRES
+ *    generatedKey[...];
+ * }
+ * + * If a predicate has alternatives, a {@link AlternativeReqPredicate} is used. + */ public class RequiredCrySLPredicate implements ISLConstraint { private final CrySLPredicate predicate; diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiredPredicateError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiredPredicateError.java new file mode 100644 index 000000000..7020cfe94 --- /dev/null +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiredPredicateError.java @@ -0,0 +1,58 @@ +package crypto.analysis.errors; + +import boomerang.scene.Statement; +import crypto.analysis.HiddenPredicate; +import crypto.analysis.IAnalysisSeed; +import crysl.rule.CrySLRule; +import java.util.Collection; +import java.util.Objects; +import java.util.Set; + +/** + * Super class for errors that work with a {@link HiddenPredicate}. Currently, there are {@link + * RequiredPredicateError} that hold errors with single violated predicates and {@link + * AlternativeReqPredicateError} that hold errors for violated predicates with alternatives. + */ +public abstract class AbstractRequiredPredicateError extends AbstractConstraintsError { + + private final Collection hiddenPredicates; + + public AbstractRequiredPredicateError( + IAnalysisSeed seed, + Statement errorStmt, + CrySLRule rule, + Collection hiddenPredicates) { + super(seed, errorStmt, rule); + + this.hiddenPredicates = Set.copyOf(hiddenPredicates); + } + + public Collection getHiddenPredicates() { + return hiddenPredicates; + } + + protected String getParamIndexAsText(int paramIndex) { + return switch (paramIndex) { + case -1 -> "Return value"; + case 0 -> "First parameter"; + case 1 -> "Second parameter"; + case 2 -> "Third parameter"; + case 3 -> "Fourth parameter"; + case 4 -> "Fifth parameter"; + case 5 -> "Sixth parameter"; + default -> (paramIndex + 1) + "th parameter"; + }; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), hiddenPredicates); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj) + && obj instanceof AbstractRequiredPredicateError other + && Objects.equals(hiddenPredicates, other.getHiddenPredicates()); + } +} diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AlternativeReqPredicateError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AlternativeReqPredicateError.java new file mode 100644 index 000000000..36518364f --- /dev/null +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AlternativeReqPredicateError.java @@ -0,0 +1,97 @@ +package crypto.analysis.errors; + +import crypto.analysis.AlternativeReqPredicate; +import crypto.analysis.AnalysisSeedWithSpecification; +import crypto.analysis.HiddenPredicate; +import crypto.analysis.RequiredCrySLPredicate; +import crysl.rule.CrySLPredicate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * Error that models a violation of required predicates with alternatives. Predicates from the + * REQUIRES section may have the form + * + *
{@code
+ * REQUIRES
+ *    generatedKey[...] || generatedPubKey[...] || generatedPrivKey[...];
+ * }
+ * + * If the base predicate "generatedKey" and any (relevant) alternative is not ensured, an error of + * this class is reported. + */ +public class AlternativeReqPredicateError extends AbstractRequiredPredicateError { + + private final Collection contradictedPredicate; + private final Collection relevantPredicates; + + public AlternativeReqPredicateError( + AnalysisSeedWithSpecification seed, + AlternativeReqPredicate violatedPred, + Collection hiddenPredicates) { + super(seed, violatedPred.getLocation(), seed.getSpecification(), hiddenPredicates); + + this.contradictedPredicate = List.copyOf(violatedPred.getAllAlternatives()); + this.relevantPredicates = Set.copyOf(violatedPred.getRelAlternatives()); + } + + public Collection getContradictedPredicate() { + return contradictedPredicate; + } + + public Collection getRelevantPredicates() { + return relevantPredicates; + } + + @Override + public String toErrorMarkerString() { + Collection added = new HashSet<>(); + + List msg = new ArrayList<>(); + for (RequiredCrySLPredicate pred : relevantPredicates) { + StringBuilder temp = new StringBuilder(); + temp.append(getParamIndexAsText(pred.getParamIndex())); + + if (pred.getPred().isNegated()) { + temp.append(" is generated as "); + } else { + temp.append(" was not properly generated as "); + } + temp.append(pred.getPred().getPredName()); + + msg.add(temp.toString()); + added.add(pred.getPred()); + } + + for (CrySLPredicate pred : contradictedPredicate) { + if (added.contains(pred)) { + continue; + } + msg.add(pred.getPredName() + " was not ensured (not relevant)"); + } + + return String.join(" AND ", msg); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), contradictedPredicate, relevantPredicates); + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj) + && obj instanceof AlternativeReqPredicateError other + && Objects.equals(contradictedPredicate, other.getContradictedPredicate()) + && Objects.equals(relevantPredicates, other.getRelevantPredicates()); + } + + @Override + public String toString() { + return "AlternativeReqPredicateError: " + toErrorMarkerString(); + } +} diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java index 84418eeda..5f120fed0 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java @@ -1,62 +1,40 @@ package crypto.analysis.errors; -import crypto.analysis.AlternativeReqPredicate; import crypto.analysis.AnalysisSeedWithSpecification; import crypto.analysis.HiddenPredicate; import crypto.analysis.RequiredCrySLPredicate; import crysl.rule.CrySLPredicate; import java.util.Collection; -import java.util.List; import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; /** - * Creates {@link RequiredPredicateError} for all Required Predicate error generates - * RequiredPredicateError + * This error models the violation of predicates from the REQUIRES section. An error is only + * reported for single predicates, that is, predicates of the form + * + *
{@code
+ * REQUIRES
+ *    generatedKey[...];
+ * }
+ * + * If a predicate has alternatives, an {@link AlternativeReqPredicateError} is reported. */ -public class RequiredPredicateError extends AbstractConstraintsError { +public class RequiredPredicateError extends AbstractRequiredPredicateError { - private final Collection contradictedPredicates; - private final Collection hiddenPredicates; + private final CrySLPredicate contradictedPredicate; private final int paramIndex; public RequiredPredicateError( AnalysisSeedWithSpecification seed, RequiredCrySLPredicate violatedPred, Collection hiddenPredicates) { - super(seed, violatedPred.getLocation(), seed.getSpecification()); + super(seed, violatedPred.getLocation(), seed.getSpecification(), hiddenPredicates); - this.hiddenPredicates = Set.copyOf(hiddenPredicates); - this.contradictedPredicates = List.of(violatedPred.getPred()); + this.contradictedPredicate = violatedPred.getPred(); this.paramIndex = violatedPred.getParamIndex(); } - public RequiredPredicateError( - AnalysisSeedWithSpecification seed, - AlternativeReqPredicate violatedPred, - Collection hiddenPredicates) { - super(seed, violatedPred.getLocation(), seed.getSpecification()); - - this.hiddenPredicates = Set.copyOf(hiddenPredicates); - this.contradictedPredicates = List.copyOf(violatedPred.getAlternatives()); - this.paramIndex = violatedPred.getParamIndex(); - } - - public void mapPrecedingErrors() { - for (HiddenPredicate hiddenPredicate : hiddenPredicates) { - Collection precedingErrors = hiddenPredicate.getPrecedingErrors(); - this.addCausingError(precedingErrors); - precedingErrors.forEach(e -> e.addSubsequentError(this)); - } - } - - public Collection getContradictedPredicates() { - return contradictedPredicates; - } - - public Collection getHiddenPredicates() { - return hiddenPredicates; + public CrySLPredicate getContradictedPredicates() { + return contradictedPredicate; } public int getParamIndex() { @@ -65,13 +43,9 @@ public int getParamIndex() { @Override public String toErrorMarkerString() { - StringBuilder msg = new StringBuilder(getParamIndexAsText()); + StringBuilder msg = new StringBuilder(getParamIndexAsText(paramIndex)); msg.append(" was not properly generated as "); - String predicateName = - getContradictedPredicates().stream() - .map(CrySLPredicate::getPredName) - .collect(Collectors.joining(" OR ")); - String[] parts = predicateName.split("(?=[A-Z])"); + String[] parts = contradictedPredicate.getPredName().split("(?=[A-Z])"); msg.append(parts[0]); for (int i = 1; i < parts.length; i++) { msg.append(parts[i]); @@ -79,30 +53,16 @@ public String toErrorMarkerString() { return msg.toString(); } - private String getParamIndexAsText() { - return switch (paramIndex) { - case -1 -> "Return value"; - case 0 -> "First parameter"; - case 1 -> "Second parameter"; - case 2 -> "Third parameter"; - case 3 -> "Fourth parameter"; - case 4 -> "Fifth parameter"; - case 5 -> "Sixth parameter"; - default -> (paramIndex + 1) + "th parameter"; - }; - } - @Override public int hashCode() { - return Objects.hash(super.hashCode(), hiddenPredicates, contradictedPredicates, paramIndex); + return Objects.hash(super.hashCode(), contradictedPredicate, paramIndex); } @Override public boolean equals(Object obj) { return super.equals(obj) && obj instanceof RequiredPredicateError other - && Objects.equals(contradictedPredicates, other.getContradictedPredicates()) - && Objects.equals(hiddenPredicates, other.getHiddenPredicates()) + && Objects.equals(contradictedPredicate, other.getContradictedPredicates()) && paramIndex == other.getParamIndex(); } diff --git a/CryptoAnalysis/src/main/java/crypto/constraints/ConstraintSolver.java b/CryptoAnalysis/src/main/java/crypto/constraints/ConstraintSolver.java index 39b13d40e..08d60c0a3 100644 --- a/CryptoAnalysis/src/main/java/crypto/constraints/ConstraintSolver.java +++ b/CryptoAnalysis/src/main/java/crypto/constraints/ConstraintSolver.java @@ -5,7 +5,6 @@ import boomerang.scene.DataFlowScope; import boomerang.scene.Statement; import boomerang.scene.sparse.SparseCFGCache; -import com.google.common.collect.Lists; import crypto.analysis.AlternativeReqPredicate; import crypto.analysis.AnalysisSeedWithSpecification; import crypto.analysis.RequiredCrySLPredicate; @@ -23,7 +22,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; +import java.util.List; public class ConstraintSolver { @@ -161,8 +162,7 @@ private void partitionConstraints() { continue; } - if (cons instanceof CrySLPredicate) { - CrySLPredicate predicate = (CrySLPredicate) cons; + if (cons instanceof CrySLPredicate predicate) { if (predefinedPreds.contains(predicate.getPredName())) { relConstraints.add(predicate); continue; @@ -178,14 +178,31 @@ private void partitionConstraints() { requiredPredicates.add(pred); } } - } else if (cons instanceof CrySLConstraint) { - ISLConstraint left = ((CrySLConstraint) cons).getLeft(); + } else if (cons instanceof CrySLConstraint constraint) { + ISLConstraint left = constraint.getLeft(); if (left instanceof CrySLPredicate && !predefinedPreds.contains(((CrySLPredicate) left).getPredName())) { - requiredPredicates.addAll( - collectAlternativePredicates( - (CrySLConstraint) cons, new ArrayList<>())); + + List allAlts = new ArrayList<>(); + extractAlternativePredicates(constraint, allAlts); + Collections.reverse(allAlts); + + if (allAlts.isEmpty()) { + continue; + } + + // Use the left pred as the base predicate to determine the statement + Collection basePreds = + retrieveValuesForPred(allAlts.get(0)); + for (RequiredCrySLPredicate reqPred : basePreds) { + Collection relAlts = + getRelevantPredicates(reqPred, allAlts); + + AlternativeReqPredicate altPred = + new AlternativeReqPredicate(reqPred, allAlts, relAlts); + requiredPredicates.add(altPred); + } } else { relConstraints.add(cons); } @@ -195,53 +212,37 @@ private void partitionConstraints() { } } - private Collection collectAlternativePredicates( - CrySLConstraint cons, Collection alts) { + private void extractAlternativePredicates(CrySLConstraint cons, List alts) { CrySLPredicate left = (CrySLPredicate) cons.getLeft(); + alts.add(left); - if (alts.isEmpty()) { - for (CallSiteWithExtractedValue callSite : this.getCollectedValues()) { - CallSiteWithParamIndex cwpi = callSite.callSiteWithParam(); - - for (ICrySLPredicateParameter p : left.getParameters()) { - if (p.getName().equals("transformation")) { - continue; - } + ISLConstraint right = cons.getRight(); + if (right instanceof CrySLPredicate predicate) { + alts.add(predicate); + } else if (right instanceof CrySLConstraint constraint) { + extractAlternativePredicates(constraint, alts); + } + } - if (cwpi.varName().equals(p.getName())) { - alts.add(new AlternativeReqPredicate(left, cwpi.statement(), cwpi.index())); - } - } - } + private Collection getRelevantPredicates( + RequiredCrySLPredicate basePred, Collection predicates) { + Collection result = new HashSet<>(); - // Extract predicates with 'this' as parameter - if (left.getParameters().stream().anyMatch(param -> param.getName().equals("this"))) { - AlternativeReqPredicate altPred = - new AlternativeReqPredicate(left, seed.getOrigin(), -1); + for (CrySLPredicate pred : predicates) { + Collection reqPreds = retrieveValuesForPred(pred); - if (!alts.contains(altPred)) { - alts.add(altPred); + for (RequiredCrySLPredicate reqPred : reqPreds) { + if (reqPred.getLocation().equals(basePred.getLocation())) { + result.add(reqPred); } } - } else { - for (AlternativeReqPredicate alt : alts) { - alt.addAlternative(left); - } } - if (cons.getRight() instanceof CrySLPredicate) { - for (AlternativeReqPredicate alt : alts) { - alt.addAlternative((CrySLPredicate) cons.getRight()); - } - } else { - return collectAlternativePredicates((CrySLConstraint) cons.getRight(), alts); - } - - return alts; + return result; } private Collection retrieveValuesForPred(CrySLPredicate pred) { - Collection result = Lists.newArrayList(); + Collection result = new ArrayList<>(); for (CallSiteWithExtractedValue callSite : this.getCollectedValues()) { CallSiteWithParamIndex cwpi = callSite.callSiteWithParam(); diff --git a/CryptoAnalysis/src/main/java/crypto/listener/AnalysisReporter.java b/CryptoAnalysis/src/main/java/crypto/listener/AnalysisReporter.java index 24f0476bc..73a796f44 100644 --- a/CryptoAnalysis/src/main/java/crypto/listener/AnalysisReporter.java +++ b/CryptoAnalysis/src/main/java/crypto/listener/AnalysisReporter.java @@ -10,6 +10,7 @@ import crypto.analysis.EnsuredCrySLPredicate; import crypto.analysis.IAnalysisSeed; import crypto.analysis.errors.AbstractError; +import crypto.analysis.errors.AlternativeReqPredicateError; import crypto.analysis.errors.CallToError; import crypto.analysis.errors.ConstraintError; import crypto.analysis.errors.ForbiddenMethodError; @@ -257,6 +258,8 @@ public void reportError(IAnalysisSeed seed, AbstractError error) { errorListener.reportError(contradictionError); } else if (error instanceof RequiredPredicateError predicateError) { errorListener.reportError(predicateError); + } else if (error instanceof AlternativeReqPredicateError predicateError) { + errorListener.reportError(predicateError); } else if (error instanceof TypestateError typestateError) { errorListener.reportError(typestateError); } else if (error instanceof UncaughtExceptionError exceptionError) { diff --git a/CryptoAnalysis/src/main/java/crypto/listener/ErrorCollector.java b/CryptoAnalysis/src/main/java/crypto/listener/ErrorCollector.java index 90e408c7f..1f901da2f 100644 --- a/CryptoAnalysis/src/main/java/crypto/listener/ErrorCollector.java +++ b/CryptoAnalysis/src/main/java/crypto/listener/ErrorCollector.java @@ -5,6 +5,7 @@ import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; import crypto.analysis.errors.AbstractError; +import crypto.analysis.errors.AlternativeReqPredicateError; import crypto.analysis.errors.CallToError; import crypto.analysis.errors.ConstraintError; import crypto.analysis.errors.ForbiddenMethodError; @@ -98,6 +99,11 @@ public void reportError(RequiredPredicateError requiredPredicateError) { addErrorToCollection(requiredPredicateError); } + @Override + public void reportError(AlternativeReqPredicateError alternativeReqPredicateError) { + addErrorToCollection(alternativeReqPredicateError); + } + @Override public void reportError(TypestateError typestateError) { addErrorToCollection(typestateError); diff --git a/CryptoAnalysis/src/main/java/crypto/listener/IErrorListener.java b/CryptoAnalysis/src/main/java/crypto/listener/IErrorListener.java index 0a75f890f..2fe5798d4 100644 --- a/CryptoAnalysis/src/main/java/crypto/listener/IErrorListener.java +++ b/CryptoAnalysis/src/main/java/crypto/listener/IErrorListener.java @@ -1,6 +1,7 @@ package crypto.listener; import crypto.analysis.errors.AbstractError; +import crypto.analysis.errors.AlternativeReqPredicateError; import crypto.analysis.errors.CallToError; import crypto.analysis.errors.ConstraintError; import crypto.analysis.errors.ForbiddenMethodError; @@ -39,6 +40,8 @@ public interface IErrorListener { void reportError(RequiredPredicateError requiredPredicateError); + void reportError(AlternativeReqPredicateError alternativeReqPredicateError); + void reportError(TypestateError typestateError); void reportError(UncaughtExceptionError uncaughtExceptionError); diff --git a/CryptoAnalysis/src/test/java/test/UsagePatternErrorListener.java b/CryptoAnalysis/src/test/java/test/UsagePatternErrorListener.java index 5a85d8c68..95e773b47 100644 --- a/CryptoAnalysis/src/test/java/test/UsagePatternErrorListener.java +++ b/CryptoAnalysis/src/test/java/test/UsagePatternErrorListener.java @@ -1,6 +1,7 @@ package test; import crypto.analysis.errors.AbstractError; +import crypto.analysis.errors.AlternativeReqPredicateError; import crypto.analysis.errors.CallToError; import crypto.analysis.errors.ConstraintError; import crypto.analysis.errors.ForbiddenMethodError; @@ -192,6 +193,16 @@ public void reportError(RequiredPredicateError requiredPredicateError) { } } + @Override + public void reportError(AlternativeReqPredicateError alternativeReqPredicateError) { + for (Assertion a : assertions) { + if (a instanceof PredicateErrorCountAssertion) { + PredicateErrorCountAssertion errorCountAssertion = (PredicateErrorCountAssertion) a; + errorCountAssertion.increaseCount(); + } + } + } + @Override public void reportError(TypestateError typestateError) { for (Assertion a : assertions) { diff --git a/CryptoAnalysis/src/test/java/test/assertions/NotHasEnsuredPredicateAssertion.java b/CryptoAnalysis/src/test/java/test/assertions/NotHasEnsuredPredicateAssertion.java index 6dc5d04bd..79e6f5696 100644 --- a/CryptoAnalysis/src/test/java/test/assertions/NotHasEnsuredPredicateAssertion.java +++ b/CryptoAnalysis/src/test/java/test/assertions/NotHasEnsuredPredicateAssertion.java @@ -51,7 +51,12 @@ public void reported(Collection seed, AbstractPredicate pred) { @Override public String toString() { if (predName == null) { - return "Did not expect a predicate for " + val.getVariableName() + " @ " + stmt + " @ line " + stmt.getStartLineNumber(); + return "Did not expect a predicate for " + + val.getVariableName() + + " @ " + + stmt + + " @ line " + + stmt.getStartLineNumber(); } else { return "Did not expect '" + predName diff --git a/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/AlternativesTest.java b/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/AlternativesTest.java deleted file mode 100644 index 41b7abc6a..000000000 --- a/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/AlternativesTest.java +++ /dev/null @@ -1,129 +0,0 @@ -package tests.error.predicate.alternatives; - -import org.junit.Test; -import test.TestConstants; -import test.UsagePatternTestingFramework; -import test.assertions.Assertions; - -public class AlternativesTest extends UsagePatternTestingFramework { - - @Override - protected String getRulesetPath() { - return TestConstants.RULES_TEST_DIR + "alternatives"; - } - - @Test - public void testPositiveSingleAlternative() { - Source source1 = new Source(); - source1.ensurePredOnThis(); - Assertions.hasEnsuredPredicate(source1); - - Target target1 = new Target(); - target1.requireSource(source1); - Assertions.hasGeneratedPredicate(target1); - - String string = "test"; - Source source2 = new Source(); - source2.ensurePredOnString(string); - Assertions.hasGeneratedPredicate(source2); - - Target target2 = new Target(); - target2.requireString(string); - Assertions.hasGeneratedPredicate(target2); - - Assertions.predicateErrors(0); - } - - @Test - public void testNegativeSingleAlternative() { - Source source1 = new Source(); - Assertions.notHasEnsuredPredicate(source1); - - Target target1 = new Target(); - target1.requireSource(source1); - Assertions.hasNotGeneratedPredicate(target1); - - String string = "test"; - Source source2 = new Source(); - Assertions.hasNotGeneratedPredicate(source2); - - Target target2 = new Target(); - target2.requireString(string); - Assertions.hasNotGeneratedPredicate(target2); - - Assertions.predicateErrors(0); - } - - @Test - public void testAlternativeWithoutStatement() { - Source source = new Source(); - Assertions.notHasEnsuredPredicate(source); - - Target target = new Target(); - target.requireSource(source); - target.requireString("test"); - - // Alternatives belong to the same predicate -> one error - Assertions.predicateErrors(1); - } - - @Test - public void testAlternativeWithSingleStatement() { - Source source1 = new Source(); - source1.ensurePredOnThis(); - Assertions.hasEnsuredPredicate(source1); - - Target target1 = new Target(); - target1.requireSource(source1); - target1.requireString("test"); - - String string = "test"; - Source source2 = new Source(); - source2.ensurePredOnString(string); - Assertions.hasEnsuredPredicate(source2); - - Target target2 = new Target(); - target2.requireSource(source2); - target2.requireString(string); - - Assertions.predicateErrors(0); - } - - @Test - public void testAlternativeWithMultipleStatements() { - String string = "test"; - Source source = new Source(); - source.ensurePredOnThis(); - source.ensurePredOnString(string); - Assertions.hasEnsuredPredicate(source); - - Target target = new Target(); - target.requireSource(source); - target.requireString(string); - - Assertions.predicateErrors(0); - } - - @Test - public void testNegativeAlternativesAtSameStatement() { - Source source = new Source(); - Assertions.notHasEnsuredPredicate(source); - - Target target = new Target(); - target.requireSourceAndString(source, "test"); - - Assertions.predicateErrors(1); - } - - @Test - public void testPositiveAlternativesAtSameStatement() { - Source source1 = new Source(); - source1.ensurePredOnThis(); - Assertions.hasEnsuredPredicate(source1); - - Target target1 = new Target(); - target1.requireSourceAndString(source1, "test"); - - Assertions.predicateErrors(0); - } -} diff --git a/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/Source.java b/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/Source.java deleted file mode 100644 index a21e44303..000000000 --- a/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/Source.java +++ /dev/null @@ -1,10 +0,0 @@ -package tests.error.predicate.alternatives; - -public class Source { - - public Source() {} - - public void ensurePredOnThis() {} - - public void ensurePredOnString(String s) {} -} diff --git a/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/Target.java b/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/Target.java deleted file mode 100644 index 15e7905c1..000000000 --- a/CryptoAnalysis/src/test/java/tests/error/predicate/alternatives/Target.java +++ /dev/null @@ -1,12 +0,0 @@ -package tests.error.predicate.alternatives; - -public class Target { - - public Target() {} - - public void requireSource(Source source) {} - - public void requireString(String string) {} - - public void requireSourceAndString(Source source, String string) {} -} diff --git a/CryptoAnalysis/src/test/java/tests/error/predicate/requiredpredicate/RequiredPredicatesTest.java b/CryptoAnalysis/src/test/java/tests/error/predicate/requiredpredicate/RequiredPredicatesTest.java index 8e5498e90..1b072e460 100644 --- a/CryptoAnalysis/src/test/java/tests/error/predicate/requiredpredicate/RequiredPredicatesTest.java +++ b/CryptoAnalysis/src/test/java/tests/error/predicate/requiredpredicate/RequiredPredicatesTest.java @@ -348,8 +348,6 @@ public void notPred1onPos1_AND_notPred2onPos2() { // OR // same predicate - @Ignore( - "Requires negated conditional predicates. Alternative predicate have to belong to the same object") @Test public void pred1onPos1_OR_pred1onPos2() { A pred1onA = new A(); @@ -377,24 +375,10 @@ public void pred1onPos1_OR_pred1onPos2() { r4.pred1onPos1_OR_pred1onPos2(noPredOnA, noPredOnA); Assertions.hasNotGeneratedPredicate(r4); - Assertions.predicateErrors(2); // two, because each parameter will be reported - } - - @Test - public void test() { - A pred1onA = new A(); - pred1onA.ensurePred1onThis(); - Assertions.hasEnsuredPredicate(pred1onA); - - A noPredOnA = new A(); - Assertions.notHasEnsuredPredicate(noPredOnA); - - Requires r2 = new Requires(); - r2.pred1onPos1_OR_pred1onPos2(pred1onA, noPredOnA); - Assertions.hasGeneratedPredicate(r2); + // one, because both parameters belong to the same alternative predicate + Assertions.predicateErrors(1); } - @Ignore @Test public void pred1onPos1_OR_notPred1onPos2() { A pred1onA = new A(); @@ -407,25 +391,25 @@ public void pred1onPos1_OR_notPred1onPos2() { // assert true Requires r1 = new Requires(); r1.pred1onPos1_OR_notPred1onPos2(pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); Requires r2 = new Requires(); r2.pred1onPos1_OR_notPred1onPos2(pred1onA, pred1onA); - Assertions.hasEnsuredPredicate(r2); + Assertions.hasGeneratedPredicate(r2); Requires r3 = new Requires(); r3.pred1onPos1_OR_notPred1onPos2(noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r3); + Assertions.hasGeneratedPredicate(r3); // assert false Requires r4 = new Requires(); r4.pred1onPos1_OR_notPred1onPos2(noPredOnA, pred1onA); - Assertions.notHasEnsuredPredicate(r4); + Assertions.hasNotGeneratedPredicate(r4); - Assertions.predicateErrors(2); // two, because each parameter will be reported + // one, because both parameters belong to the same alternative predicate + Assertions.predicateErrors(1); } - @Ignore @Test public void notPred1onPos1_OR_pred1onPos2() { A pred1onA = new A(); @@ -438,25 +422,25 @@ public void notPred1onPos1_OR_pred1onPos2() { // assert true Requires r1 = new Requires(); r1.notPred1onPos1_OR_pred1onPos2(noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); Requires r2 = new Requires(); r2.notPred1onPos1_OR_pred1onPos2(noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r2); + Assertions.hasGeneratedPredicate(r2); Requires r3 = new Requires(); r3.notPred1onPos1_OR_pred1onPos2(pred1onA, pred1onA); - Assertions.hasEnsuredPredicate(r3); + Assertions.hasGeneratedPredicate(r3); // assert false Requires r4 = new Requires(); r4.notPred1onPos1_OR_pred1onPos2(pred1onA, noPredOnA); - Assertions.notHasEnsuredPredicate(r4); + Assertions.hasNotGeneratedPredicate(r4); - Assertions.predicateErrors(2); // two, because each parameter will be reported + // one, because both parameters belong to the same alternative predicate + Assertions.predicateErrors(1); } - @Ignore @Test public void notPred1onPos1_OR_notPred1onPos2() { A pred1onA = new A(); @@ -473,28 +457,29 @@ public void notPred1onPos1_OR_notPred1onPos2() { // assert true Requires r1 = new Requires(); r1.notPred1onPos1_OR_notPred1onPos2(noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); Requires r2 = new Requires(); r2.notPred1onPos1_OR_notPred1onPos2(noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r2); + Assertions.hasGeneratedPredicate(r2); Requires r3 = new Requires(); r3.notPred1onPos1_OR_notPred1onPos2(pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r3); + Assertions.hasGeneratedPredicate(r3); // assert false Requires r4 = new Requires(); r4.notPred1onPos1_OR_notPred1onPos2(pred1onA, pred1onA2); - Assertions.notHasEnsuredPredicate(r4); + Assertions.hasNotGeneratedPredicate(r4); - Assertions.predicateErrors(2); // two, because each parameter will be reported + // one, because both parameters belong to the same alternative predicate + Assertions.predicateErrors(1); } // multi predicates @Ignore( - "Can be tested since negated conditions in the REQUIRES section are not supported" - + "Alternative predicates on different objects o1 and o2 (p1[o1] || p2[o2] have to be rewritten as" + "Cannot be tested since negated conditions in the REQUIRES section are not supported" + + "Alternative predicates on different objects o1 and o2 (p1[o1] || p2[o2]) have to be rewritten as" + "!p1[o1] => p2[o2]; and !p2[o2] => p1[o1];") @Test public void pred1onPos1_OR_pred2onPos2() { @@ -512,25 +497,25 @@ public void pred1onPos1_OR_pred2onPos2() { // assert true Requires r1 = new Requires(); r1.pred1onPos1_OR_pred2onPos2(pred1onA, pred2onA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); Requires r2 = new Requires(); r2.pred1onPos1_OR_pred2onPos2(pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r2); + Assertions.hasGeneratedPredicate(r2); Requires r3 = new Requires(); r3.pred1onPos1_OR_pred2onPos2(noPredOnA, pred2onA); - Assertions.hasEnsuredPredicate(r3); + Assertions.hasGeneratedPredicate(r3); // assert false Requires r4 = new Requires(); r4.pred1onPos1_OR_pred2onPos2(noPredOnA, noPredOnA); - Assertions.notHasEnsuredPredicate(r4); + Assertions.hasNotGeneratedPredicate(r4); - Assertions.predicateErrors(2); // two, because each parameter will be reported + // one, because both parameters belong to the same alternative predicate + Assertions.predicateErrors(1); } - @Ignore("Predicate that are not checked yet are assumed to be true") @Test public void pred1onP1_OR_notPred2onP2() { A pred1onA = new A(); @@ -547,25 +532,25 @@ public void pred1onP1_OR_notPred2onP2() { // assert true Requires r1 = new Requires(); r1.pred1onPos1_OR_notPred2onPos2(pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); Requires r2 = new Requires(); r2.pred1onPos1_OR_notPred2onPos2(pred1onA, pred2onA); - Assertions.hasEnsuredPredicate(r2); + Assertions.hasGeneratedPredicate(r2); Requires r3 = new Requires(); r3.pred1onPos1_OR_notPred2onPos2(noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r3); + Assertions.hasGeneratedPredicate(r3); // assert false Requires r4 = new Requires(); r4.pred1onPos1_OR_notPred2onPos2(noPredOnA, pred2onA); - Assertions.notHasEnsuredPredicate(r4); + Assertions.hasNotGeneratedPredicate(r4); - Assertions.predicateErrors(2); // two, because each parameter will be reported + // one, because both parameters belong to the same alternative predicate + Assertions.predicateErrors(1); } - @Ignore("Predicate that are not checked yet are assumed to be true") @Test public void notPred1onP1_OR_pred2onP2() { A pred1onA = new A(); @@ -582,25 +567,25 @@ public void notPred1onP1_OR_pred2onP2() { // assert true Requires r1 = new Requires(); r1.notPred1onPos1_OR_pred2onPos2(noPredOnA, pred2onA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); Requires r2 = new Requires(); r2.notPred1onPos1_OR_pred2onPos2(pred1onA, pred2onA); - Assertions.hasEnsuredPredicate(r2); + Assertions.hasGeneratedPredicate(r2); Requires r3 = new Requires(); r3.notPred1onPos1_OR_pred2onPos2(noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r3); + Assertions.hasGeneratedPredicate(r3); // assert false Requires r4 = new Requires(); r4.notPred1onPos1_OR_pred2onPos2(pred1onA, noPredOnA); - Assertions.notHasEnsuredPredicate(r4); + Assertions.hasNotGeneratedPredicate(r4); - Assertions.predicateErrors(2); // two, because each parameter will be reported + // one, because both parameters belong to the same alternative predicate + Assertions.predicateErrors(1); } - @Ignore("Predicate that are not checked yet are assumed to be true") @Test public void notPred1onP1_OR_notPred2onP2() { A pred1onA = new A(); @@ -617,26 +602,26 @@ public void notPred1onP1_OR_notPred2onP2() { // assert true Requires r1 = new Requires(); r1.notPred1onPos1_OR_notPred2onPos2(noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); Requires r2 = new Requires(); r2.notPred1onPos1_OR_notPred2onPos2(pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r2); + Assertions.hasGeneratedPredicate(r2); Requires r3 = new Requires(); r3.notPred1onPos1_OR_notPred2onPos2(noPredOnA, pred2onA); - Assertions.hasEnsuredPredicate(r3); + Assertions.hasGeneratedPredicate(r3); // assert false Requires r4 = new Requires(); r4.notPred1onPos1_OR_notPred2onPos2(pred1onA, pred2onA); - Assertions.notHasEnsuredPredicate(r4); + Assertions.hasNotGeneratedPredicate(r4); - Assertions.predicateErrors(2); // two, because each parameter will be reported + // one, because both parameters belong to the same alternative predicate + Assertions.predicateErrors(1); } // 3 cases same predicate - @Ignore("Negated conditions are not supported see above") @Test public void pred1onPos1_OR_pred1onPos2_OR_pred1onPos3() { A pred1onA = new A(); @@ -649,40 +634,41 @@ public void pred1onPos1_OR_pred1onPos2_OR_pred1onPos3() { // assert true Requires r1 = new Requires(); r1.pred1onPos1_OR_pred1onPos2_OR_pred1onPos3(pred1onA, pred1onA, pred1onA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); Requires r2 = new Requires(); r2.pred1onPos1_OR_pred1onPos2_OR_pred1onPos3(noPredOnA, pred1onA, pred1onA); - Assertions.hasEnsuredPredicate(r2); + Assertions.hasGeneratedPredicate(r2); Requires r3 = new Requires(); r3.pred1onPos1_OR_pred1onPos2_OR_pred1onPos3(pred1onA, noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r3); + Assertions.hasGeneratedPredicate(r3); Requires r4 = new Requires(); r4.pred1onPos1_OR_pred1onPos2_OR_pred1onPos3(pred1onA, pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r4); + Assertions.hasGeneratedPredicate(r4); + Requires r5 = new Requires(); r5.pred1onPos1_OR_pred1onPos2_OR_pred1onPos3(noPredOnA, noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r5); + Assertions.hasGeneratedPredicate(r5); Requires r6 = new Requires(); r6.pred1onPos1_OR_pred1onPos2_OR_pred1onPos3(pred1onA, noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r6); + Assertions.hasGeneratedPredicate(r6); Requires r7 = new Requires(); r7.pred1onPos1_OR_pred1onPos2_OR_pred1onPos3(noPredOnA, pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r7); + Assertions.hasGeneratedPredicate(r7); // assert false Requires r8 = new Requires(); r8.pred1onPos1_OR_pred1onPos2_OR_pred1onPos3(noPredOnA, noPredOnA, noPredOnA); - Assertions.notHasEnsuredPredicate(r8); + Assertions.hasNotGeneratedPredicate(r8); - Assertions.predicateErrors(3); // three, because each parameter will be reported + // one, because all three parameters belong to the same alternative predicate + Assertions.predicateErrors(1); } - @Ignore @Test public void pred1onPos1_OR_notPred1onPos2_OR_pred1onPos3() { A pred1onA = new A(); @@ -695,41 +681,41 @@ public void pred1onPos1_OR_notPred1onPos2_OR_pred1onPos3() { // assert true Requires r1 = new Requires(); r1.pred1onPos1_OR_notPred1onPos2_OR_pred1onPos3(pred1onA, pred1onA, pred1onA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); Requires r2 = new Requires(); r2.pred1onPos1_OR_notPred1onPos2_OR_pred1onPos3(noPredOnA, pred1onA, pred1onA); - Assertions.hasEnsuredPredicate(r2); + Assertions.hasGeneratedPredicate(r2); Requires r3 = new Requires(); r3.pred1onPos1_OR_notPred1onPos2_OR_pred1onPos3(pred1onA, noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r3); + Assertions.hasGeneratedPredicate(r3); Requires r4 = new Requires(); r4.pred1onPos1_OR_notPred1onPos2_OR_pred1onPos3(pred1onA, pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r4); + Assertions.hasGeneratedPredicate(r4); Requires r5 = new Requires(); r5.pred1onPos1_OR_notPred1onPos2_OR_pred1onPos3(noPredOnA, noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r5); + Assertions.hasGeneratedPredicate(r5); Requires r6 = new Requires(); r6.pred1onPos1_OR_notPred1onPos2_OR_pred1onPos3(pred1onA, noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r6); + Assertions.hasGeneratedPredicate(r6); Requires r7 = new Requires(); r7.pred1onPos1_OR_notPred1onPos2_OR_pred1onPos3(noPredOnA, noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r7); + Assertions.hasGeneratedPredicate(r7); // assert false Requires r8 = new Requires(); r8.pred1onPos1_OR_notPred1onPos2_OR_pred1onPos3(noPredOnA, pred1onA, noPredOnA); - Assertions.notHasEnsuredPredicate(r8); + Assertions.hasNotGeneratedPredicate(r8); - Assertions.predicateErrors(3); // three, because each parameter will be reported + // one, because all three parameters belong to the same alternative predicate + Assertions.predicateErrors(1); } - @Ignore @Test public void notPred1onPos1_OR_pred1onPos2_OR_pred1onPos3() { A pred1onA = new A(); @@ -742,41 +728,41 @@ public void notPred1onPos1_OR_pred1onPos2_OR_pred1onPos3() { // assert true Requires r1 = new Requires(); r1.notPred1onPos1_OR_pred1onPos2_OR_pred1onPos3(pred1onA, pred1onA, pred1onA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); Requires r2 = new Requires(); r2.notPred1onPos1_OR_pred1onPos2_OR_pred1onPos3(noPredOnA, pred1onA, pred1onA); - Assertions.hasEnsuredPredicate(r2); + Assertions.hasGeneratedPredicate(r2); Requires r3 = new Requires(); r3.notPred1onPos1_OR_pred1onPos2_OR_pred1onPos3(pred1onA, noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r3); + Assertions.hasGeneratedPredicate(r3); Requires r4 = new Requires(); r4.notPred1onPos1_OR_pred1onPos2_OR_pred1onPos3(pred1onA, pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r4); + Assertions.hasGeneratedPredicate(r4); Requires r5 = new Requires(); r5.notPred1onPos1_OR_pred1onPos2_OR_pred1onPos3(noPredOnA, noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r5); + Assertions.hasGeneratedPredicate(r5); Requires r6 = new Requires(); r6.notPred1onPos1_OR_pred1onPos2_OR_pred1onPos3(noPredOnA, pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r6); + Assertions.hasGeneratedPredicate(r6); Requires r7 = new Requires(); r7.notPred1onPos1_OR_pred1onPos2_OR_pred1onPos3(noPredOnA, noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r7); + Assertions.hasGeneratedPredicate(r7); // assert false Requires r8 = new Requires(); r8.notPred1onPos1_OR_pred1onPos2_OR_pred1onPos3(pred1onA, noPredOnA, noPredOnA); - Assertions.notHasEnsuredPredicate(r8); + Assertions.hasNotGeneratedPredicate(r8); - Assertions.predicateErrors(3); // three, because each parameter will be reported + // one, because all three parameters belong to the same alternative predicate + Assertions.predicateErrors(1); } - @Ignore @Test public void notPred1onPos1_OR_notPred1onPos2_OR_pred1onPos3() { A pred1onA = new A(); @@ -789,41 +775,41 @@ public void notPred1onPos1_OR_notPred1onPos2_OR_pred1onPos3() { // assert true Requires r1 = new Requires(); r1.notPred1onPos1_OR_notPred1onPos2_OR_pred1onPos3(pred1onA, pred1onA, pred1onA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); Requires r2 = new Requires(); r2.notPred1onPos1_OR_notPred1onPos2_OR_pred1onPos3(noPredOnA, pred1onA, pred1onA); - Assertions.hasEnsuredPredicate(r2); + Assertions.hasGeneratedPredicate(r2); Requires r3 = new Requires(); r3.notPred1onPos1_OR_notPred1onPos2_OR_pred1onPos3(pred1onA, noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r3); + Assertions.hasGeneratedPredicate(r3); Requires r4 = new Requires(); r4.notPred1onPos1_OR_notPred1onPos2_OR_pred1onPos3(pred1onA, noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r4); + Assertions.hasGeneratedPredicate(r4); Requires r5 = new Requires(); r5.notPred1onPos1_OR_notPred1onPos2_OR_pred1onPos3(noPredOnA, noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r5); + Assertions.hasGeneratedPredicate(r5); Requires r6 = new Requires(); r6.notPred1onPos1_OR_notPred1onPos2_OR_pred1onPos3(noPredOnA, pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r6); + Assertions.hasGeneratedPredicate(r6); Requires r7 = new Requires(); r7.notPred1onPos1_OR_notPred1onPos2_OR_pred1onPos3(noPredOnA, noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r7); + Assertions.hasGeneratedPredicate(r7); // assert false Requires r8 = new Requires(); r8.notPred1onPos1_OR_notPred1onPos2_OR_pred1onPos3(pred1onA, pred1onA, noPredOnA); - Assertions.notHasEnsuredPredicate(r8); + Assertions.hasNotGeneratedPredicate(r8); - Assertions.predicateErrors(3); // three, because each parameter will be reported + // one, because all three parameters belong to the same alternative predicate + Assertions.predicateErrors(1); } - @Ignore @Test public void pred1onPos1_OR_pred1onPos2_OR_notPred1onPos3() { A pred1onA = new A(); @@ -836,42 +822,41 @@ public void pred1onPos1_OR_pred1onPos2_OR_notPred1onPos3() { // assert true Requires r1 = new Requires(); r1.pred1onPos1_OR_pred1onPos2_OR_notPred1onPos3(pred1onA, pred1onA, pred1onA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); Requires r2 = new Requires(); r2.pred1onPos1_OR_pred1onPos2_OR_notPred1onPos3(noPredOnA, pred1onA, pred1onA); - Assertions.hasEnsuredPredicate(r2); + Assertions.hasGeneratedPredicate(r2); Requires r3 = new Requires(); r3.pred1onPos1_OR_pred1onPos2_OR_notPred1onPos3(pred1onA, noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r3); + Assertions.hasGeneratedPredicate(r3); Requires r4 = new Requires(); r4.pred1onPos1_OR_pred1onPos2_OR_notPred1onPos3(pred1onA, noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r4); + Assertions.hasGeneratedPredicate(r4); Requires r5 = new Requires(); r5.pred1onPos1_OR_pred1onPos2_OR_notPred1onPos3(pred1onA, pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r5); + Assertions.hasGeneratedPredicate(r5); Requires r6 = new Requires(); r6.pred1onPos1_OR_pred1onPos2_OR_notPred1onPos3(noPredOnA, pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r6); + Assertions.hasGeneratedPredicate(r6); + Requires r7 = new Requires(); r7.pred1onPos1_OR_pred1onPos2_OR_notPred1onPos3(noPredOnA, noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r7); + Assertions.hasGeneratedPredicate(r7); // assert false Requires r8 = new Requires(); r8.pred1onPos1_OR_pred1onPos2_OR_notPred1onPos3(noPredOnA, noPredOnA, pred1onA); - Assertions.notHasEnsuredPredicate(r8); + Assertions.hasNotGeneratedPredicate(r8); - Assertions.hasEnsuredPredicate(pred1onA); - Assertions.notHasEnsuredPredicate(noPredOnA); - Assertions.predicateErrors(3); // three, because each parameter will be reported + // one, because all three parameters belong to the same alternative predicate + Assertions.predicateErrors(1); } - @Ignore @Test public void pred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3() { A pred1onA = new A(); @@ -884,41 +869,41 @@ public void pred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3() { // assert true Requires r1 = new Requires(); r1.pred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3(pred1onA, pred1onA, pred1onA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); Requires r2 = new Requires(); r2.pred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3(noPredOnA, noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r2); + Assertions.hasGeneratedPredicate(r2); Requires r3 = new Requires(); r3.pred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3(pred1onA, noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r3); + Assertions.hasGeneratedPredicate(r3); Requires r4 = new Requires(); r4.pred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3(pred1onA, noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r4); + Assertions.hasGeneratedPredicate(r4); Requires r5 = new Requires(); r5.pred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3(pred1onA, pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r5); + Assertions.hasGeneratedPredicate(r5); Requires r6 = new Requires(); r6.pred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3(noPredOnA, pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r6); + Assertions.hasGeneratedPredicate(r6); Requires r7 = new Requires(); r7.pred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3(noPredOnA, noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r7); + Assertions.hasGeneratedPredicate(r7); // assert false Requires r8 = new Requires(); r8.pred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3(noPredOnA, pred1onA, pred1onA); - Assertions.notHasEnsuredPredicate(r8); + Assertions.hasNotGeneratedPredicate(r8); - Assertions.predicateErrors(3); // three, because each parameter will be reported + // one, because all three parameters belong to the same alternative predicate + Assertions.predicateErrors(1); } - @Ignore @Test public void notPred1onPos1_OR_pred1onPos2_OR_notPred1onPos3() { A pred1onA = new A(); @@ -931,39 +916,41 @@ public void notPred1onPos1_OR_pred1onPos2_OR_notPred1onPos3() { // assert true Requires r1 = new Requires(); r1.notPred1onPos1_OR_pred1onPos2_OR_notPred1onPos3(pred1onA, pred1onA, pred1onA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); Requires r2 = new Requires(); r2.notPred1onPos1_OR_pred1onPos2_OR_notPred1onPos3(noPredOnA, noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r2); + Assertions.hasGeneratedPredicate(r2); Requires r3 = new Requires(); r3.notPred1onPos1_OR_pred1onPos2_OR_notPred1onPos3(noPredOnA, pred1onA, pred1onA); - Assertions.hasEnsuredPredicate(r3); + Assertions.hasGeneratedPredicate(r3); Requires r4 = new Requires(); r4.notPred1onPos1_OR_pred1onPos2_OR_notPred1onPos3(pred1onA, noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r4); + Assertions.hasGeneratedPredicate(r4); + Requires r5 = new Requires(); r5.notPred1onPos1_OR_pred1onPos2_OR_notPred1onPos3(pred1onA, pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r5); + Assertions.hasGeneratedPredicate(r5); Requires r6 = new Requires(); r6.notPred1onPos1_OR_pred1onPos2_OR_notPred1onPos3(noPredOnA, pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r6); + Assertions.hasGeneratedPredicate(r6); + Requires r7 = new Requires(); r7.notPred1onPos1_OR_pred1onPos2_OR_notPred1onPos3(noPredOnA, noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r7); + Assertions.hasGeneratedPredicate(r7); // assert false Requires r8 = new Requires(); r8.notPred1onPos1_OR_pred1onPos2_OR_notPred1onPos3(pred1onA, noPredOnA, pred1onA); - Assertions.notHasEnsuredPredicate(r8); + Assertions.hasNotGeneratedPredicate(r8); - Assertions.predicateErrors(3); // three, because each parameter will be reported + // one, because all three parameters belong to the same alternative predicate + Assertions.predicateErrors(1); } - @Ignore @Test public void notPred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3() { A pred1onA = new A(); @@ -976,38 +963,39 @@ public void notPred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3() { // assert true Requires r1 = new Requires(); r1.notPred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3(pred1onA, noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r1); + Assertions.hasGeneratedPredicate(r1); Requires r2 = new Requires(); r2.notPred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3(noPredOnA, noPredOnA, pred1onA); - Assertions.hasEnsuredPredicate(r2); + Assertions.hasGeneratedPredicate(r2); Requires r3 = new Requires(); r3.notPred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3(noPredOnA, pred1onA, pred1onA); - Assertions.hasEnsuredPredicate(r3); + Assertions.hasGeneratedPredicate(r3); Requires r4 = new Requires(); r4.notPred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3(pred1onA, noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r4); + Assertions.hasGeneratedPredicate(r4); Requires r5 = new Requires(); r5.notPred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3(pred1onA, pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r5); + Assertions.hasGeneratedPredicate(r5); Requires r6 = new Requires(); r6.notPred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3(noPredOnA, pred1onA, noPredOnA); - Assertions.hasEnsuredPredicate(r6); + Assertions.hasGeneratedPredicate(r6); Requires r7 = new Requires(); r7.notPred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3(noPredOnA, noPredOnA, noPredOnA); - Assertions.hasEnsuredPredicate(r7); + Assertions.hasGeneratedPredicate(r7); // assert false Requires r8 = new Requires(); r8.notPred1onPos1_OR_notPred1onPos2_OR_notPred1onPos3(pred1onA, pred1onA, pred1onA); - Assertions.notHasEnsuredPredicate(r8); + Assertions.hasNotGeneratedPredicate(r8); - Assertions.predicateErrors(3); // three, because each parameter will be reported + // one, because all three parameters belong to the same alternative predicate + Assertions.predicateErrors(1); } // IMPLICATE diff --git a/CryptoAnalysis/src/test/resources/testrules/alternatives/Source.crysl b/CryptoAnalysis/src/test/resources/testrules/alternatives/Source.crysl deleted file mode 100644 index 373bc32bc..000000000 --- a/CryptoAnalysis/src/test/resources/testrules/alternatives/Source.crysl +++ /dev/null @@ -1,13 +0,0 @@ -SPEC tests.error.predicate.alternatives.Source - -OBJECTS - java.lang.String string; - -EVENTS - Con: Source(); - EnsOnThis: ensurePredOnThis(); - EnsOnString: ensurePredOnString(string); - -ENSURES - predSource[this] after EnsOnThis; - predString[string]; diff --git a/CryptoAnalysis/src/test/resources/testrules/alternatives/Target.crysl b/CryptoAnalysis/src/test/resources/testrules/alternatives/Target.crysl deleted file mode 100644 index edb8baea6..000000000 --- a/CryptoAnalysis/src/test/resources/testrules/alternatives/Target.crysl +++ /dev/null @@ -1,17 +0,0 @@ -SPEC tests.error.predicate.alternatives.Target - -OBJECTS - tests.error.predicate.alternatives.Source source; - java.lang.String string; - -EVENTS - Con: Target(); - ReqSource: requireSource(source); - ReqString: requireString(string); - ReqSourceAndString: requireSourceAndString(source, string); - -REQUIRES - predSource[source] || predString[string]; - -ENSURES - generatedTarget[this]; diff --git a/HeadlessJavaScanner/src/test/java/scanner/targets/BouncyCastleHeadlessTest.java b/HeadlessJavaScanner/src/test/java/scanner/targets/BouncyCastleHeadlessTest.java index c15c0a6b7..25729efb5 100644 --- a/HeadlessJavaScanner/src/test/java/scanner/targets/BouncyCastleHeadlessTest.java +++ b/HeadlessJavaScanner/src/test/java/scanner/targets/BouncyCastleHeadlessTest.java @@ -1,5 +1,6 @@ package scanner.targets; +import crypto.analysis.errors.AlternativeReqPredicateError; import crypto.analysis.errors.ForbiddenMethodError; import crypto.analysis.errors.HardCodedError; import crypto.analysis.errors.ImpreciseValueExtractionError; @@ -41,7 +42,8 @@ public void testBCMacExamples() { new ErrorSpecification.Builder("gwt_crypto.GMacTest", "performTestOne", 0) .withTPs(ForbiddenMethodError.class, 1) .withTPs(NeverTypeOfError.class, 1) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 1) .withTPs(IncompleteOperationError.class, 1) .build()); addErrorSpecification( @@ -78,7 +80,8 @@ public void testBCSymmetricCipherExamples() { addErrorSpecification( new ErrorSpecification.Builder( "gcm_aes_example.GCMAESBouncyCastle", "processing", 2) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder( @@ -113,18 +116,18 @@ public void testBCAsymmetricCipherExamples() { .build()); addErrorSpecification( new ErrorSpecification.Builder("rsa_misuse.RSATest", "Decrypt", 2) - .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); // These two errors occur because AsymmetricCipherKeyPair ensures the predicate only after // the constructor call addErrorSpecification( new ErrorSpecification.Builder("rsa_nomisuse.RSATest", "Encrypt", 2) - .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder("rsa_nomisuse.RSATest", "Decrypt", 2) - .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( @@ -133,7 +136,8 @@ public void testBCAsymmetricCipherExamples() { .build()); addErrorSpecification( new ErrorSpecification.Builder("crypto.RSAEngineTest", "testDecryptOne", 1) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 1) .withTPs(ImpreciseValueExtractionError.class, 1) .build()); addErrorSpecification( @@ -247,13 +251,15 @@ public void testBCSignerExamples() { addErrorSpecification( new ErrorSpecification.Builder("gwt_crypto.PSSBlindTest", "testSig", 6) .withTPs(IncompleteOperationError.class, 1) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(ImpreciseValueExtractionError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder("gwt_crypto.PSSTest", "testSig", 6) .withTPs(IncompleteOperationError.class, 1) - .withTPs(RequiredPredicateError.class, 2) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder( @@ -275,12 +281,12 @@ public void testBCSignerExamples() { addErrorSpecification( new ErrorSpecification.Builder("diqube.TicketSignatureService", "signTicket", 0) - .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder( "diqube.TicketSignatureService", "isValidTicketSignature", 1) - .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .withTPs(ImpreciseValueExtractionError.class, 1) .build()); @@ -295,11 +301,13 @@ public void testBCSignerExamples() { addErrorSpecification( new ErrorSpecification.Builder("pattern.SignerTest", "testSignerGenerate", 0) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder("pattern.SignerTest", "testSignerVerify", 0) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); scanner.run(); @@ -315,7 +323,7 @@ public void testBCEllipticCurveExamples() { addErrorSpecification( new ErrorSpecification.Builder("crypto.ECElGamalEncryptorTest", "testOne", 0) - .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder("crypto.ECElGamalEncryptorTest", "testTwo", 0) @@ -354,11 +362,13 @@ public void testBCEllipticCurveExamples() { .build()); addErrorSpecification( new ErrorSpecification.Builder("params.ParametersWithRandomTest", "testOne", 1) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder("params.ParametersWithRandomTest", "testThree", 1) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder("params.ECDomainParametersTest", "testThree", 1) @@ -403,12 +413,14 @@ public void testBCEllipticCurveExamples() { addErrorSpecification( new ErrorSpecification.Builder( "transforms.ECNewPublicKeyTransformTest", "testOne", 1) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder( "transforms.ECNewPublicKeyTransformTest", "testTwo", 1) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .build()); addErrorSpecification( new ErrorSpecification.Builder( @@ -419,28 +431,32 @@ public void testBCEllipticCurveExamples() { new ErrorSpecification.Builder( "transforms.ECNewPublicKeyTransformTest", "testFour", 1) .withTPs(IncompleteOperationError.class, 1) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .build()); addErrorSpecification( new ErrorSpecification.Builder( "transforms.ECNewPublicKeyTransformTest", "testFive", 1) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .build()); addErrorSpecification( new ErrorSpecification.Builder( "transforms.ECNewPublicKeyTransformTest", "testSix", 1) - .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .build()); addErrorSpecification( new ErrorSpecification.Builder( "transforms.ECNewRandomessTransformTest", "testOne", 1) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder( "transforms.ECNewRandomessTransformTest", "testTwo", 1) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .build()); addErrorSpecification( new ErrorSpecification.Builder( @@ -451,17 +467,19 @@ public void testBCEllipticCurveExamples() { new ErrorSpecification.Builder( "transforms.ECNewRandomessTransformTest", "testFour", 1) .withTPs(IncompleteOperationError.class, 1) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .build()); addErrorSpecification( new ErrorSpecification.Builder( "transforms.ECNewRandomessTransformTest", "testFive", 1) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .build()); addErrorSpecification( new ErrorSpecification.Builder( "transforms.ECNewRandomessTransformTest", "testSix", 1) - .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .build()); addErrorSpecification( diff --git a/HeadlessJavaScanner/src/test/java/scanner/targets/BragaCryptoGoodUsesTest.java b/HeadlessJavaScanner/src/test/java/scanner/targets/BragaCryptoGoodUsesTest.java index 1f6f07e42..27b388333 100644 --- a/HeadlessJavaScanner/src/test/java/scanner/targets/BragaCryptoGoodUsesTest.java +++ b/HeadlessJavaScanner/src/test/java/scanner/targets/BragaCryptoGoodUsesTest.java @@ -1,5 +1,6 @@ package scanner.targets; +import crypto.analysis.errors.AlternativeReqPredicateError; import crypto.analysis.errors.CallToError; import crypto.analysis.errors.ConstraintError; import crypto.analysis.errors.ForbiddenMethodError; @@ -66,7 +67,8 @@ public void alwaysDefineCSPExamples() { new ErrorSpecification.Builder("example.DefinedProvider7", "main", 1) .withTPs(ConstraintError.class, 1) .withTPs(IncompleteOperationError.class, 2) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .build()); scanner.run(); @@ -88,7 +90,8 @@ public void avoidCodingErrorsExamples() { new ErrorSpecification.Builder("example.PBEwLargeCountAndRandomSalt", "main", 1) .withTPs(ConstraintError.class, 1) .withTPs(TypestateError.class, 1) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(ForbiddenMethodError.class, 1) .withTPs(IncompleteOperationError.class, 1) .build()); @@ -123,7 +126,8 @@ public void avoidConstantPwdPBEExamples() { new ErrorSpecification.Builder("example.PBEwParameterPassword", "main", 1) .withTPs(ConstraintError.class, 1) .withTPs(TypestateError.class, 1) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(ForbiddenMethodError.class, 1) .withTPs(IncompleteOperationError.class, 1) .build()); @@ -155,7 +159,8 @@ public void avoidDeterministicRSAExamples() { new ErrorSpecification.Builder("example.UseOAEPForRSA", "negativeTestCase", 0) .withTPs(ConstraintError.class, 3) .withTPs(IncompleteOperationError.class, 2) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .build()); // positive test case @@ -170,7 +175,8 @@ public void avoidDeterministicRSAExamples() { new ErrorSpecification.Builder("example.UsePKCS1ForRSA", "negativeTestCase", 0) .withTPs(ConstraintError.class, 3) .withTPs(IncompleteOperationError.class, 2) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .build()); scanner.run(); @@ -211,7 +217,8 @@ public void avoidHardcodedKeysExamples() { addErrorSpecification( new ErrorSpecification.Builder("example.UseDynamicKeyFor3DES", "main", 1) .withTPs(ConstraintError.class, 3) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); addErrorSpecification( @@ -258,7 +265,8 @@ public void avoidImproperKeyLenExamples() { new ErrorSpecification.Builder( "example.SecureConfig112bitsRSA_2048x256_1", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -275,7 +283,8 @@ public void avoidImproperKeyLenExamples() { new ErrorSpecification.Builder( "example.SecureConfig112bitsRSA_2048x256_2", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -349,7 +358,8 @@ public void avoidImproperKeyLenExamples() { new ErrorSpecification.Builder( "example.SecureConfig192bitsRSA_7680x384_1", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -369,7 +379,8 @@ public void avoidImproperKeyLenExamples() { new ErrorSpecification.Builder( "example.SecureConfig192bitsRSA_7680x512_1", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -391,7 +402,8 @@ public void avoidInsecureDefaultsExamples() { addErrorSpecification( new ErrorSpecification.Builder("example.UseQualifiedNameForPBE1", "main", 1) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(ForbiddenMethodError.class, 1) .withTPs(TypestateError.class, 1) .withTPs(IncompleteOperationError.class, 1) @@ -425,7 +437,8 @@ public void avoidInsecureDefaultsExamples() { "example.UseQualifiedNameForRSAOAEP", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) .withTPs(TypestateError.class, 1) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .build()); // positive test case @@ -443,7 +456,8 @@ public void avoidInsecureDefaultsExamples() { "example.UseQualifiedParamsForRSAOAEP", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) .withTPs(TypestateError.class, 1) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .build()); scanner.run(); @@ -522,7 +536,8 @@ public void avoidInsecurePaddingExamples() { addErrorSpecification( new ErrorSpecification.Builder("example.OAEP_2048x256_1", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -537,7 +552,8 @@ public void avoidInsecurePaddingExamples() { addErrorSpecification( new ErrorSpecification.Builder("example.OAEP_2048x256_2", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -555,7 +571,8 @@ public void avoidInsecurePaddingExamples() { addErrorSpecification( new ErrorSpecification.Builder("example.OAEP_2048x384_1", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -570,7 +587,8 @@ public void avoidInsecurePaddingExamples() { addErrorSpecification( new ErrorSpecification.Builder("example.OAEP_2048x384_2", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -588,7 +606,8 @@ public void avoidInsecurePaddingExamples() { addErrorSpecification( new ErrorSpecification.Builder("example.OAEP_2048x512_1", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -603,7 +622,8 @@ public void avoidInsecurePaddingExamples() { addErrorSpecification( new ErrorSpecification.Builder("example.OAEP_2048x512_2", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1105,7 +1125,8 @@ public void doNotPrintSecretsExamples() { addErrorSpecification( new ErrorSpecification.Builder("example.DoNotPrintPrivKey1", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1218,7 +1239,8 @@ public void secureConfigsRSAExamples() { new ErrorSpecification.Builder( "example.SecureConfig112bitsRSA_2048x256_1", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1235,7 +1257,8 @@ public void secureConfigsRSAExamples() { new ErrorSpecification.Builder( "example.SecureConfig112bitsRSA_2048x256_2", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1309,7 +1332,8 @@ public void secureConfigsRSAExamples() { new ErrorSpecification.Builder( "example.SecureConfig192bitsRSA_7680x384_1", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1326,7 +1350,8 @@ public void secureConfigsRSAExamples() { new ErrorSpecification.Builder( "example.SecureConfig192bitsRSA_7680x384_2", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1346,7 +1371,8 @@ public void secureConfigsRSAExamples() { new ErrorSpecification.Builder( "example.SecureConfig192bitsRSA_7680x512_1", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1363,7 +1389,8 @@ public void secureConfigsRSAExamples() { new ErrorSpecification.Builder( "example.SecureConfig192bitsRSA_7680x512_2", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); diff --git a/HeadlessJavaScanner/src/test/java/scanner/targets/BragaCryptoMisusesTest.java b/HeadlessJavaScanner/src/test/java/scanner/targets/BragaCryptoMisusesTest.java index 279d60f0d..a9273fadb 100644 --- a/HeadlessJavaScanner/src/test/java/scanner/targets/BragaCryptoMisusesTest.java +++ b/HeadlessJavaScanner/src/test/java/scanner/targets/BragaCryptoMisusesTest.java @@ -1,5 +1,6 @@ package scanner.targets; +import crypto.analysis.errors.AlternativeReqPredicateError; import crypto.analysis.errors.CallToError; import crypto.analysis.errors.ConstraintError; import crypto.analysis.errors.ForbiddenMethodError; @@ -228,23 +229,27 @@ public void constantKeyExamples() { addErrorSpecification( new ErrorSpecification.Builder("pkm.constantKey.ConstantKey3DES", "main", 1) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder("pkm.constantKey.ConstantKeyAES1", "main", 1) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder("pkm.constantKey.ConstantKeyAES2", "main", 1) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .withTPs(NeverTypeOfError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder("pkm.constantKey.ConstantKeyAES3", "main", 1) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .withTPs(NeverTypeOfError.class, 1) .build()); @@ -275,7 +280,8 @@ public void constPwd4PBEExamples() { addErrorSpecification( new ErrorSpecification.Builder("pkm.constPwd4PBE.ConstPassword4PBE1", "main", 1) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .withTPs(ForbiddenMethodError.class, 1) .withTPs(IncompleteOperationError.class, 1) @@ -284,7 +290,8 @@ public void constPwd4PBEExamples() { addErrorSpecification( new ErrorSpecification.Builder("pkm.constPwd4PBE.ConstPassword4PBE2", "main", 1) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .withTPs(ForbiddenMethodError.class, 1) .withTPs(IncompleteOperationError.class, 1) @@ -307,7 +314,8 @@ public void customCryptoExamples() { addErrorSpecification( new ErrorSpecification.Builder("wc.customCrypto.Manual3DES", "main", 1) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -352,7 +360,8 @@ public void deterministicCryptoExamples() { new ErrorSpecification.Builder( "pkc.enc.deterministicCrypto.DeterministicEncryptionRSA", "main", 1) .withTPs(ConstraintError.class, 7) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(IncompleteOperationError.class, 2) .build()); addErrorSpecification( @@ -361,7 +370,8 @@ public void deterministicCryptoExamples() { "main", 1) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(IncompleteOperationError.class, 2) .build()); addErrorSpecification( @@ -370,7 +380,8 @@ public void deterministicCryptoExamples() { "main", 1) .withTPs(ConstraintError.class, 3) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(IncompleteOperationError.class, 2) .build()); addErrorSpecification( @@ -379,7 +390,8 @@ public void deterministicCryptoExamples() { "main", 1) .withTPs(ConstraintError.class, 3) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(IncompleteOperationError.class, 2) .build()); @@ -705,7 +717,8 @@ public void insecureDefaultExamples() { addErrorSpecification( new ErrorSpecification.Builder("pdf.insecureDefault.InsecureDefault3DES", "main", 1) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); addErrorSpecification( @@ -719,7 +732,8 @@ public void insecureDefaultExamples() { new ErrorSpecification.Builder( "pdf.insecureDefault.InsecureDefaultOAEP", "positiveTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -728,7 +742,8 @@ public void insecureDefaultExamples() { new ErrorSpecification.Builder( "pdf.insecureDefault.InsecureDefaultOAEP", "negativeTestCase", 0) .withTPs(ConstraintError.class, 3) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -744,7 +759,8 @@ public void insecureDefaultExamples() { new ErrorSpecification.Builder( "pdf.insecureDefault.InsecureDefaultRSA", "negativeTestCase", 0) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(IncompleteOperationError.class, 2) .build()); @@ -778,7 +794,8 @@ public void insecurePaddingExamples() { "negativeTestCase", 0) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(IncompleteOperationError.class, 2) .build()); @@ -799,7 +816,8 @@ public void insecurePaddingExamples() { "negativeTestCase", 0) .withTPs(ConstraintError.class, 3) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(IncompleteOperationError.class, 2) .build()); @@ -820,7 +838,8 @@ public void insecurePaddingExamples() { "negativeTestCase", 0) .withTPs(ConstraintError.class, 3) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(IncompleteOperationError.class, 2) .build()); @@ -983,14 +1002,16 @@ public void insecureStreamCipherExamples() { addErrorSpecification( new ErrorSpecification.Builder( "pdf.insecureStreamCipher.ConfusingBlockAndStream", "main", 1) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .withTPs(NeverTypeOfError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder( "pdf.insecureStreamCipher.MalealableStreamCipher", "main", 1) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 2) .withTPs(IncompleteOperationError.class, 1) .withTPs(NeverTypeOfError.class, 1) @@ -1099,7 +1120,8 @@ public void keyReuseInStreamCipherExamples() { addErrorSpecification( new ErrorSpecification.Builder( "pkm.keyReuseInStreamCipher.KeyReuseStreamCipher1", "main", 1) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .withTPs(CallToError.class, 1) .withTPs(NeverTypeOfError.class, 1) @@ -1107,28 +1129,32 @@ public void keyReuseInStreamCipherExamples() { addErrorSpecification( new ErrorSpecification.Builder( "pkm.keyReuseInStreamCipher.KeyReuseStreamCipher2", "main", 1) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .withTPs(CallToError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder( "pkm.keyReuseInStreamCipher.KeyReuseStreamCipher3", "main", 1) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .withTPs(CallToError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder( "pkm.keyReuseInStreamCipher.KeyReuseStreamCipher4", "main", 1) - .withTPs(RequiredPredicateError.class, 8) + .withTPs(RequiredPredicateError.class, 6) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .withTPs(CallToError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder( "pkm.keyReuseInStreamCipher.KeyReuseStreamCipher5", "main", 1) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .withTPs(CallToError.class, 1) .build()); @@ -1150,7 +1176,8 @@ public void nonceReuseExamples() { addErrorSpecification( new ErrorSpecification.Builder("ivm.nonceReuse.NonceReuse1", "main", 1) .withTPs(TypestateError.class, 1) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(CallToError.class, 1) .build()); @@ -1185,7 +1212,8 @@ public void paramsPBEExamples() { addErrorSpecification( new ErrorSpecification.Builder("cib.paramsPBE.PBEwConstSalt1", "main", 1) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(ForbiddenMethodError.class, 1) .withTPs(TypestateError.class, 1) .withTPs(IncompleteOperationError.class, 1) @@ -1194,7 +1222,8 @@ public void paramsPBEExamples() { addErrorSpecification( new ErrorSpecification.Builder("cib.paramsPBE.PBEwSmallCount1", "main", 1) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(ForbiddenMethodError.class, 1) .withTPs(TypestateError.class, 1) .withTPs(IncompleteOperationError.class, 1) @@ -1203,7 +1232,8 @@ public void paramsPBEExamples() { addErrorSpecification( new ErrorSpecification.Builder("cib.paramsPBE.PBEwSmallSalt", "main", 1) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(ForbiddenMethodError.class, 1) .withTPs(TypestateError.class, 1) .withTPs(IncompleteOperationError.class, 1) @@ -1287,7 +1317,8 @@ public void printPrivSecKeyExamples() { new ErrorSpecification.Builder( "cib.printPrivSecKey.PrintPrivKey1", "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1357,7 +1388,8 @@ public void riskyInsecureCryptoExamples() { new ErrorSpecification.Builder( "wc.riskyInsecureCrypto.InsecureCrypto3DES", "main", 1) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1365,7 +1397,8 @@ public void riskyInsecureCryptoExamples() { new ErrorSpecification.Builder( "wc.riskyInsecureCrypto.InsecureCryptoBlowfish", "main", 1) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1373,7 +1406,8 @@ public void riskyInsecureCryptoExamples() { new ErrorSpecification.Builder( "wc.riskyInsecureCrypto.InsecureCryptoDES", "main", 1) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1381,7 +1415,8 @@ public void riskyInsecureCryptoExamples() { new ErrorSpecification.Builder( "wc.riskyInsecureCrypto.InsecureCryptoDES_StreamCipher", "main", 1) .withTPs(ConstraintError.class, 5) - .withTPs(RequiredPredicateError.class, 12) + .withTPs(RequiredPredicateError.class, 4) + .withTPs(AlternativeReqPredicateError.class, 8) .withTPs(TypestateError.class, 5) .build()); @@ -1389,7 +1424,8 @@ public void riskyInsecureCryptoExamples() { new ErrorSpecification.Builder( "wc.riskyInsecureCrypto.InsecureCryptoRC4_StreamCipher", "main", 1) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1423,14 +1459,16 @@ public void sideChannelAttacksExamples() { addErrorSpecification( new ErrorSpecification.Builder("pdf.sideChannelAttacks.PaddingOracle", "oracle", 2) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 2) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder( "pdf.sideChannelAttacks.PaddingOracle", "encripta", 0) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 2) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( @@ -1515,13 +1553,15 @@ public void undefinedCSPExamples() { addErrorSpecification( new ErrorSpecification.Builder("cai.undefinedCSP.UndefinedProvider7", "main", 1) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder("cai.undefinedCSP.UndefinedProvider8", "main", 1) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(IncompleteOperationError.class, 2) .build()); @@ -1546,7 +1586,8 @@ public void weakConfigsRSAExamples() { "positiveTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 6) + .withTPs(RequiredPredicateError.class, 4) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1557,7 +1598,8 @@ public void weakConfigsRSAExamples() { "negativeTestCase", 0) .withTPs(ConstraintError.class, 3) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1568,7 +1610,8 @@ public void weakConfigsRSAExamples() { "positiveTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 6) + .withTPs(RequiredPredicateError.class, 4) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1579,7 +1622,8 @@ public void weakConfigsRSAExamples() { "negativeTestCase", 0) .withTPs(ConstraintError.class, 3) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1590,7 +1634,8 @@ public void weakConfigsRSAExamples() { "positiveTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 6) + .withTPs(RequiredPredicateError.class, 4) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1601,7 +1646,8 @@ public void weakConfigsRSAExamples() { "negativeTestCase", 0) .withTPs(ConstraintError.class, 3) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1612,7 +1658,8 @@ public void weakConfigsRSAExamples() { "positiveTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 6) + .withTPs(RequiredPredicateError.class, 4) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1623,7 +1670,8 @@ public void weakConfigsRSAExamples() { "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1634,7 +1682,8 @@ public void weakConfigsRSAExamples() { "positiveTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 6) + .withTPs(RequiredPredicateError.class, 4) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1645,7 +1694,8 @@ public void weakConfigsRSAExamples() { "negativeTestCase", 0) .withTPs(ConstraintError.class, 3) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1656,7 +1706,8 @@ public void weakConfigsRSAExamples() { "positiveTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 6) + .withTPs(RequiredPredicateError.class, 4) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1667,7 +1718,8 @@ public void weakConfigsRSAExamples() { "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1678,7 +1730,8 @@ public void weakConfigsRSAExamples() { "positiveTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 6) + .withTPs(RequiredPredicateError.class, 4) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1689,7 +1742,8 @@ public void weakConfigsRSAExamples() { "negativeTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1700,7 +1754,8 @@ public void weakConfigsRSAExamples() { "positiveTestCase", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 6) + .withTPs(RequiredPredicateError.class, 4) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); @@ -1711,7 +1766,8 @@ public void weakConfigsRSAExamples() { "negativeTestCase", 0) .withTPs(ConstraintError.class, 3) - .withTPs(RequiredPredicateError.class, 7) + .withTPs(RequiredPredicateError.class, 5) + .withTPs(AlternativeReqPredicateError.class, 2) .withTPs(TypestateError.class, 1) .build()); diff --git a/HeadlessJavaScanner/src/test/java/scanner/targets/CogniCryptGeneratedCodeTest.java b/HeadlessJavaScanner/src/test/java/scanner/targets/CogniCryptGeneratedCodeTest.java index 29aa6717e..8d5fa156e 100644 --- a/HeadlessJavaScanner/src/test/java/scanner/targets/CogniCryptGeneratedCodeTest.java +++ b/HeadlessJavaScanner/src/test/java/scanner/targets/CogniCryptGeneratedCodeTest.java @@ -1,5 +1,6 @@ package scanner.targets; +import crypto.analysis.errors.AlternativeReqPredicateError; import crypto.analysis.errors.CallToError; import crypto.analysis.errors.HardCodedError; import crypto.analysis.errors.RequiredPredicateError; @@ -31,11 +32,12 @@ public void fileEncryptor() { .build()); addErrorSpecification( new ErrorSpecification.Builder("Crypto.Enc", "encrypt", 2) - .withFPs(RequiredPredicateError.class, 1, "Mystery") + .withFPs(AlternativeReqPredicateError.class, 1, "Mystery") .build()); addErrorSpecification( new ErrorSpecification.Builder("Crypto.Enc", "decrypt", 2) - .withFPs(RequiredPredicateError.class, 2, "Mystery") + .withFPs(RequiredPredicateError.class, 1, "Mystery") + .withFPs(AlternativeReqPredicateError.class, 1, "Mystery") .build()); scanner.run(); diff --git a/HeadlessJavaScanner/src/test/java/scanner/targets/CryptoGuardTest.java b/HeadlessJavaScanner/src/test/java/scanner/targets/CryptoGuardTest.java index 83214db1c..88b26149b 100644 --- a/HeadlessJavaScanner/src/test/java/scanner/targets/CryptoGuardTest.java +++ b/HeadlessJavaScanner/src/test/java/scanner/targets/CryptoGuardTest.java @@ -1,5 +1,6 @@ package scanner.targets; +import crypto.analysis.errors.AlternativeReqPredicateError; import crypto.analysis.errors.ConstraintError; import crypto.analysis.errors.HardCodedError; import crypto.analysis.errors.ImpreciseValueExtractionError; @@ -48,7 +49,8 @@ public void brokenCryptoExamples() { new ErrorSpecification.Builder( "example.brokencrypto.BrokenCryptoABICase2", "doCrypto", 1) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 2) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .withTPs(IncompleteOperationError.class, 1) .build()); // ABICase3, ABICase4, ABICase9 not included as tests due to being similar to ABICase1 and @@ -60,7 +62,8 @@ public void brokenCryptoExamples() { new ErrorSpecification.Builder( "example.brokencrypto.BrokenCryptoABICase5", "doCrypto", 0) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 2) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .withTPs(IncompleteOperationError.class, 1) .withTPs(ImpreciseValueExtractionError.class, 1) .build()); @@ -71,7 +74,8 @@ public void brokenCryptoExamples() { addErrorSpecification( new ErrorSpecification.Builder("example.brokencrypto.BrokenCryptoBBCase3", "go", 0) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 2) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .withTPs(IncompleteOperationError.class, 1) .build()); // BBCase1, BBCase2, BBCase4, BBCase5 not included as tests due to being similar to BBCase3 @@ -192,7 +196,8 @@ public void insecureAsymmetricCryptoExamples() { "go", 2) .withTPs(IncompleteOperationError.class, 2) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .build()); // This test case corresponds to the following project in CryptoGuard: @@ -211,9 +216,10 @@ public void insecureAsymmetricCryptoExamples() { "go", 2) .withTPs(IncompleteOperationError.class, 2) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 2) .build()); - // In the case above, misuse is caught correctly, but the keysize is reported to be 0 + // In the case above, misuse is caught correctly, but the key size is reported to be 0 // and not 1024, as it really is. This is caused because of the structure of the project // as explained in the issue: https://github.com/CROSSINGTUD/CryptoAnalysis/issues/163 @@ -226,7 +232,8 @@ public void insecureAsymmetricCryptoExamples() { 0) .withTPs(ConstraintError.class, 1) .withTPs(IncompleteOperationError.class, 2) - .withTPs(RequiredPredicateError.class, 5) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 2) .build()); scanner.run(); diff --git a/HeadlessJavaScanner/src/test/java/scanner/targets/IgnoreSectionsTest.java b/HeadlessJavaScanner/src/test/java/scanner/targets/IgnoreSectionsTest.java index a52b70c18..f52df1c9f 100644 --- a/HeadlessJavaScanner/src/test/java/scanner/targets/IgnoreSectionsTest.java +++ b/HeadlessJavaScanner/src/test/java/scanner/targets/IgnoreSectionsTest.java @@ -1,5 +1,6 @@ package scanner.targets; +import crypto.analysis.errors.AlternativeReqPredicateError; import crypto.analysis.errors.ConstraintError; import crypto.analysis.errors.IncompleteOperationError; import crypto.analysis.errors.RequiredPredicateError; @@ -39,7 +40,8 @@ public void ignoreNoPackages() { addErrorSpecification( new ErrorSpecification.Builder("example.PredicateMissingExample", "main", 1) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 2) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder("example.TypestateErrorExample", "main", 1) @@ -116,7 +118,8 @@ public void ignoreClassesExample() { addErrorSpecification( new ErrorSpecification.Builder("example.PredicateMissingExample", "main", 1) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 2) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder("example.TypestateErrorExample", "main", 1) diff --git a/HeadlessJavaScanner/src/test/java/scanner/targets/MUBenchExamplesTest.java b/HeadlessJavaScanner/src/test/java/scanner/targets/MUBenchExamplesTest.java index e2934d5e0..6d6257f39 100644 --- a/HeadlessJavaScanner/src/test/java/scanner/targets/MUBenchExamplesTest.java +++ b/HeadlessJavaScanner/src/test/java/scanner/targets/MUBenchExamplesTest.java @@ -1,5 +1,6 @@ package scanner.targets; +import crypto.analysis.errors.AlternativeReqPredicateError; import crypto.analysis.errors.ConstraintError; import crypto.analysis.errors.IncompleteOperationError; import crypto.analysis.errors.NeverTypeOfError; @@ -81,7 +82,8 @@ public void muBenchExamples() { addErrorSpecification( new ErrorSpecification.Builder("example.CipherUsesNonRandomKeyExample", "main", 1) .withTPs(NeverTypeOfError.class, 1) - .withTPs(RequiredPredicateError.class, 2) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); // This test case corresponds to the following project in MUBench having this misuse: diff --git a/HeadlessJavaScanner/src/test/java/scanner/targets/ReportedIssueTest.java b/HeadlessJavaScanner/src/test/java/scanner/targets/ReportedIssueTest.java index 9313843b0..f43500db5 100644 --- a/HeadlessJavaScanner/src/test/java/scanner/targets/ReportedIssueTest.java +++ b/HeadlessJavaScanner/src/test/java/scanner/targets/ReportedIssueTest.java @@ -1,5 +1,6 @@ package scanner.targets; +import crypto.analysis.errors.AlternativeReqPredicateError; import crypto.analysis.errors.ConstraintError; import crypto.analysis.errors.HardCodedError; import crypto.analysis.errors.IncompleteOperationError; @@ -46,7 +47,7 @@ public void reportedIssues() { addErrorSpecification( new ErrorSpecification.Builder("issue81.Encryption", "encrypt", 2) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder("issue81.Encryption", "generateKey", 1) @@ -71,13 +72,15 @@ public void reportedIssues() { addErrorSpecification( new ErrorSpecification.Builder("issue70.ClientProtocolDecoder", "decryptAES", 1) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder("issue69.Issue69", "encryptByPublicKey", 1) .withTPs(IncompleteOperationError.class, 1) - .withTPs(RequiredPredicateError.class, 4) + .withTPs(RequiredPredicateError.class, 3) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( @@ -93,7 +96,7 @@ public void reportedIssues() { .build()); addErrorSpecification( new ErrorSpecification.Builder("issue68.AESCryptor", "encryptImpl", 1) - .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder("issue68.AESCryptor", "", 1) @@ -102,7 +105,8 @@ public void reportedIssues() { .build()); addErrorSpecification( new ErrorSpecification.Builder("issue68.AESCryptor", "decryptImpl", 1) - .withTPs(RequiredPredicateError.class, 2) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( new ErrorSpecification.Builder( @@ -166,7 +170,7 @@ public void issue271Test() { } @Test - public void issue270() { + public void issue270Test() { // Related to https://github.com/CROSSINGTUD/CryptoAnalysis/issues/270 String mavenProjectPath = new File("../CryptoAnalysisTargets/Bugfixes/issue270").getAbsolutePath(); diff --git a/HeadlessJavaScanner/src/test/java/scanner/targets/SootJava9ConfigurationTest.java b/HeadlessJavaScanner/src/test/java/scanner/targets/SootJava9ConfigurationTest.java index 0fc3eddd7..0679d0242 100644 --- a/HeadlessJavaScanner/src/test/java/scanner/targets/SootJava9ConfigurationTest.java +++ b/HeadlessJavaScanner/src/test/java/scanner/targets/SootJava9ConfigurationTest.java @@ -2,6 +2,7 @@ import static org.junit.Assume.assumeTrue; +import crypto.analysis.errors.AlternativeReqPredicateError; import crypto.analysis.errors.ConstraintError; import crypto.analysis.errors.IncompleteOperationError; import crypto.analysis.errors.RequiredPredicateError; @@ -67,7 +68,8 @@ public void testJava8Project() { addErrorSpecification( new ErrorSpecification.Builder("example.PredicateMissingExample", "main", 1) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 2) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( diff --git a/HeadlessJavaScanner/src/test/java/scanner/targets/StaticAnalysisDemoTest.java b/HeadlessJavaScanner/src/test/java/scanner/targets/StaticAnalysisDemoTest.java index 9c00506f5..b14996f9c 100644 --- a/HeadlessJavaScanner/src/test/java/scanner/targets/StaticAnalysisDemoTest.java +++ b/HeadlessJavaScanner/src/test/java/scanner/targets/StaticAnalysisDemoTest.java @@ -1,5 +1,6 @@ package scanner.targets; +import crypto.analysis.errors.AlternativeReqPredicateError; import crypto.analysis.errors.ConstraintError; import crypto.analysis.errors.HardCodedError; import crypto.analysis.errors.ImpreciseValueExtractionError; @@ -32,7 +33,8 @@ public void cogniCryptDemoExamples() { addErrorSpecification( new ErrorSpecification.Builder("example.PredicateMissingExample", "main", 1) .withTPs(ConstraintError.class, 2) - .withTPs(RequiredPredicateError.class, 2) + .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( @@ -123,7 +125,7 @@ public void glassfishExample() { new ErrorSpecification.Builder( "org.glassfish.grizzly.config.ssl.CustomClass", "init", 2) .withTPs(ConstraintError.class, 1) - .withTPs(RequiredPredicateError.class, 1) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( @@ -150,7 +152,8 @@ public void oracleExample() { .withTPs(ConstraintError.class, 2) .withTPs(TypestateError.class, 1) .withTPs(NeverTypeOfError.class, 1) - .withTPs(RequiredPredicateError.class, 3) + .withTPs(RequiredPredicateError.class, 2) + .withTPs(AlternativeReqPredicateError.class, 1) .build()); addErrorSpecification( From 54d26f7551f07e1cb22c12f26bd6a04a2f989289 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Thu, 19 Dec 2024 13:17:16 +0100 Subject: [PATCH 24/32] Update Android tests --- .../src/test/java/android/HeadlessAndroidTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/HeadlessAndroidScanner/src/test/java/android/HeadlessAndroidTest.java b/HeadlessAndroidScanner/src/test/java/android/HeadlessAndroidTest.java index 76f3e5616..519295c10 100644 --- a/HeadlessAndroidScanner/src/test/java/android/HeadlessAndroidTest.java +++ b/HeadlessAndroidScanner/src/test/java/android/HeadlessAndroidTest.java @@ -1,5 +1,6 @@ package android; +import crypto.analysis.errors.AlternativeReqPredicateError; import crypto.analysis.errors.ConstraintError; import crypto.analysis.errors.ImpreciseValueExtractionError; import crypto.analysis.errors.IncompleteOperationError; @@ -17,9 +18,10 @@ public void testFalseCrypt() { scanner.run(); addExpectedErrors(ConstraintError.class, 6); - addExpectedErrors(RequiredPredicateError.class, 9); + addExpectedErrors(RequiredPredicateError.class, 7); + addExpectedErrors(AlternativeReqPredicateError.class, 2); addExpectedErrors(TypestateError.class, 1); - addExpectedErrors(IncompleteOperationError.class, 5); + addExpectedErrors(IncompleteOperationError.class, 4); addExpectedErrors(ImpreciseValueExtractionError.class, 1); assertErrors(scanner.getCollectedErrors()); From dae19fa37956d77c4cefb38871925c15229fce55 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Thu, 19 Dec 2024 16:51:56 +0100 Subject: [PATCH 25/32] Updates colors --- .../main/java/crypto/visualization/WrappedCluster.java | 4 ++-- .../src/main/java/crypto/visualization/WrappedNode.java | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CryptoAnalysis/src/main/java/crypto/visualization/WrappedCluster.java b/CryptoAnalysis/src/main/java/crypto/visualization/WrappedCluster.java index 5f63135c2..57c9c89ab 100644 --- a/CryptoAnalysis/src/main/java/crypto/visualization/WrappedCluster.java +++ b/CryptoAnalysis/src/main/java/crypto/visualization/WrappedCluster.java @@ -28,10 +28,10 @@ public Cluster asGraphicalCluster() { cluster = Cluster.builder() .subgraph(subgraph) - .label(seed.getFact().getVariableName()) + .label(" " + seed.getFact().getVariableName()) .labeljust(Labeljust.LEFT) .shape(ClusterShapeEnum.RECT) - .bgColor(Color.GREY) + .bgColor(Color.ofRGB("#E8E8E8")) // Gray-ish .build(); } diff --git a/CryptoAnalysis/src/main/java/crypto/visualization/WrappedNode.java b/CryptoAnalysis/src/main/java/crypto/visualization/WrappedNode.java index 9cef949f4..327335f5b 100644 --- a/CryptoAnalysis/src/main/java/crypto/visualization/WrappedNode.java +++ b/CryptoAnalysis/src/main/java/crypto/visualization/WrappedNode.java @@ -3,6 +3,7 @@ import crypto.analysis.errors.AbstractConstraintsError; import crypto.analysis.errors.AbstractError; import crypto.analysis.errors.AbstractOrderError; +import crypto.analysis.errors.AbstractRequiredPredicateError; import crypto.analysis.errors.ForbiddenMethodError; import crypto.analysis.errors.PredicateConstraintError; import java.util.Objects; @@ -42,11 +43,13 @@ private String getLabel() { private Color getColor() { if (error instanceof ForbiddenMethodError) { - return Color.BLUE; + return Color.ofRGB("#9AF6FF"); // turquoise-ish } else if (error instanceof AbstractOrderError) { - return Color.ORANGE; + return Color.ofRGB("#FFF7AB"); // Yellow-ish + } else if (error instanceof AbstractRequiredPredicateError) { + return Color.ofRGB("#FFEBB2"); // Orange-ish } else if (error instanceof AbstractConstraintsError) { - return Color.YELLOW; + return Color.ofRGB("#C1D4FF"); // Blue-ish } else if (error instanceof PredicateConstraintError) { return Color.INDIGO; } else { From cb06485ed50496068edbc7cd7a7a1895068de76e Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Fri, 20 Dec 2024 13:25:59 +0100 Subject: [PATCH 26/32] Add classes to nodes and update README --- .../src/main/java/crypto/visualization/WrappedNode.java | 2 ++ README.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CryptoAnalysis/src/main/java/crypto/visualization/WrappedNode.java b/CryptoAnalysis/src/main/java/crypto/visualization/WrappedNode.java index 327335f5b..ec4217ef9 100644 --- a/CryptoAnalysis/src/main/java/crypto/visualization/WrappedNode.java +++ b/CryptoAnalysis/src/main/java/crypto/visualization/WrappedNode.java @@ -35,6 +35,8 @@ public Node asGraphicalNode() { private String getLabel() { return error.getClass().getSimpleName() + + "\nClass: " + + error.getErrorStatement().getMethod().getDeclaringClass() + "\n" + error.getErrorStatement() + "\nLine: " diff --git a/README.md b/README.md index dea79faf2..ad3e9dfdb 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Other additional arguments that can be used are as follows: --identifier --reportPath --reportFormat (possible values are CMD, TXT, SARIF, CSV, CSV_SUMMARY) ---visualization (enables the visualization, but also requires --reportPath option to be set) +--visualization (Create a visualization of all errors (requires --reportPath option to be set)) --dstats (disables the output of the analysis statistics in the reports) --ignoreSections (Text file with packages (e.g. `de.example.*`), classes (e.g. `de.example.exmapleClass`) or methods (e.g. `de.example.exampleClass.exampleMethod`), one per line. Those packages, classes and methods are ignored during the analysis) --timeout (Timeout for seeds in milliseconds. If a seed exceeds this value, CryptoAnalysis aborts the typestate and extract parameter analysis and continues with the results computed so far. (default: 10000)) From af57d09d7b01a24683c4abb5790e412fe9505f23 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Fri, 20 Dec 2024 14:17:45 +0100 Subject: [PATCH 27/32] Rename ensured and hidden predicates -EnsuresCrySLPredicate -> EnsuredPredicate -HiddenPredicate -> UnEnsuredPredicate --- .../AnalysisSeedWithSpecification.java | 74 +++++++++---------- ...SLPredicate.java => EnsuredPredicate.java} | 6 +- .../crypto/analysis/PredicateHandler.java | 8 +- ...Predicate.java => UnEnsuredPredicate.java} | 6 +- .../AbstractRequiredPredicateError.java | 18 ++--- .../errors/AlternativeReqPredicateError.java | 6 +- .../errors/RequiredPredicateError.java | 6 +- .../crypto/listener/AnalysisReporter.java | 4 +- .../crypto/listener/IResultsListener.java | 4 +- .../test/UsagePatternResultsListener.java | 12 +-- .../HasEnsuredPredicateAssertion.java | 4 +- .../HasGeneratedPredicateAssertion.java | 4 +- .../HasNotGeneratedPredicateAssertion.java | 4 +- .../NotHasEnsuredPredicateAssertion.java | 4 +- 14 files changed, 80 insertions(+), 80 deletions(-) rename CryptoAnalysis/src/main/java/crypto/analysis/{EnsuredCrySLPredicate.java => EnsuredPredicate.java} (77%) rename CryptoAnalysis/src/main/java/crypto/analysis/{HiddenPredicate.java => UnEnsuredPredicate.java} (95%) diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java b/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java index 9bed5bb99..78ae3adfb 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java @@ -57,9 +57,9 @@ public class AnalysisSeedWithSpecification extends IAnalysisSeed { private final Map allCallsOnObject; private final Collection requiringSeeds = new HashSet<>(); - private final Multimap> ensuredPredicates = + private final Multimap> ensuredPredicates = HashMultimap.create(); - private final Multimap> hiddenPredicates = + private final Multimap> hiddenPredicates = HashMultimap.create(); private final Collection indirectlyEnsuredPredicates = new HashSet<>(); @@ -564,19 +564,19 @@ public void ensurePredicates() { } for (CrySLPredicate predToBeEnsured : predsToBeEnsured) { - Collection violations = new HashSet<>(); + Collection violations = new HashSet<>(); if (errorCollection.stream().anyMatch(e -> e instanceof ForbiddenMethodError)) { - violations.add(HiddenPredicate.Violations.CallToForbiddenMethod); + violations.add(UnEnsuredPredicate.Violations.CallToForbiddenMethod); } if (!satisfiesConstraintSystem) { - violations.add(HiddenPredicate.Violations.ConstraintsAreNotSatisfied); + violations.add(UnEnsuredPredicate.Violations.ConstraintsAreNotSatisfied); } if (predToBeEnsured.getConstraint().isPresent() && isPredConditionViolated(predToBeEnsured)) { - violations.add(HiddenPredicate.Violations.ConditionIsNotSatisfied); + violations.add(UnEnsuredPredicate.Violations.ConditionIsNotSatisfied); } for (Statement statement : expectedPredStatements) { @@ -600,9 +600,9 @@ && isPredConditionViolated(predToBeEnsured)) { states.stream() .anyMatch(s -> !doesStateGeneratePredicate(s, predToBeEnsured)); - Collection allViolations = new HashSet<>(violations); + Collection allViolations = new HashSet<>(violations); if (allStatesNonGenerating) { - allViolations.add(HiddenPredicate.Violations.GeneratingStateIsNeverReached); + allViolations.add(UnEnsuredPredicate.Violations.GeneratingStateIsNeverReached); } else if (someStatesNonGenerating) { /* TODO * Due to a bug, IDEal returns the states [0,1] whenever there is a @@ -615,14 +615,14 @@ && isPredConditionViolated(predToBeEnsured)) { AbstractPredicate generatedPred; if (!allViolations.isEmpty()) { generatedPred = - new HiddenPredicate( + new UnEnsuredPredicate( predToBeEnsured.toNormalCrySLPredicate(), constraintSolver.getCollectedValues(), this, allViolations); } else { generatedPred = - new EnsuredCrySLPredicate( + new EnsuredPredicate( predToBeEnsured.toNormalCrySLPredicate(), constraintSolver.getCollectedValues()); } @@ -710,13 +710,13 @@ private Collection getStatesAtStatement(Statement statement) { public void addEnsuredPredicate( AbstractPredicate ensPred, Statement statement, int paramIndex) { - if (ensPred instanceof HiddenPredicate hiddenPredicate) { - Map.Entry predAtIndex = - new AbstractMap.SimpleEntry<>(hiddenPredicate, paramIndex); + if (ensPred instanceof UnEnsuredPredicate unEnsuredPredicate) { + Map.Entry predAtIndex = + new AbstractMap.SimpleEntry<>(unEnsuredPredicate, paramIndex); hiddenPredicates.put(statement, predAtIndex); - } else if (ensPred instanceof EnsuredCrySLPredicate ensuredCrySLPredicate) { - Map.Entry predAtIndex = - new AbstractMap.SimpleEntry<>(ensuredCrySLPredicate, paramIndex); + } else if (ensPred instanceof EnsuredPredicate ensuredPredicate) { + Map.Entry predAtIndex = + new AbstractMap.SimpleEntry<>(ensuredPredicate, paramIndex); ensuredPredicates.put(statement, predAtIndex); } } @@ -887,10 +887,10 @@ public Collection computeViolatedRequiredPredicates() { } // Check for basic required predicates, e.g. randomized - Collection> predsAtStatement = + Collection> predsAtStatement = ensuredPredicates.get(reqPred.getLocation()); int reqParamIndex = reqPred.getParamIndex(); - for (Map.Entry ensPredAtIndex : predsAtStatement) { + for (Map.Entry ensPredAtIndex : predsAtStatement) { if (doReqPredAndEnsPredMatch(reqPred.getPred(), reqParamIndex, ensPredAtIndex)) { remainingPredicates.remove(reqPred); } @@ -942,9 +942,9 @@ public Collection computeViolatedAlternativePredicates( CrySLPredicate predicate = positive.getPred(); int paramIndex = positive.getParamIndex(); - Collection> predsAtStatement = + Collection> predsAtStatement = ensuredPredicates.get(altPred.getLocation()); - for (Map.Entry ensPred : predsAtStatement) { + for (Map.Entry ensPred : predsAtStatement) { if (doReqPredAndEnsPredMatch(predicate, paramIndex, ensPred)) { satisfied = true; } @@ -958,9 +958,9 @@ public Collection computeViolatedAlternativePredicates( CrySLPredicate predicate = negative.getPred(); int paramIndex = negative.getParamIndex(); - Collection> predsAtStatement = + Collection> predsAtStatement = ensuredPredicates.get(altPred.getLocation()); - for (Map.Entry ensPred : predsAtStatement) { + for (Map.Entry ensPred : predsAtStatement) { if (doReqPredAndEnsPredMatch(predicate, paramIndex, ensPred)) { ensuredNegatives.remove(negative); } @@ -994,10 +994,10 @@ public Collection computeContradictedPredicates() { // Check for basic negated required predicates, e.g. randomized CrySLPredicate invertedPred = reqPred.getPred().invertNegation(); - Collection> predsAtStatement = + Collection> predsAtStatement = ensuredPredicates.get(reqPred.getLocation()); - for (Map.Entry ensPredAtIndex : predsAtStatement) { + for (Map.Entry ensPredAtIndex : predsAtStatement) { if (doReqPredAndEnsPredMatch( invertedPred, reqPred.getParamIndex(), ensPredAtIndex)) { contradictedPredicates.add(reqPred); @@ -1012,7 +1012,7 @@ public Collection computeContradictedPredicates() { private boolean doReqPredAndEnsPredMatch( CrySLPredicate reqPred, int reqPredIndex, - Map.Entry ensPred) { + Map.Entry ensPred) { CrySLPredicate predToCheck; if (reqPred.isNegated()) { predToCheck = reqPred.invertNegation(); @@ -1091,11 +1091,11 @@ private boolean doPredsMatch(CrySLPredicate pred, AbstractPredicate ensPred) { return requiredPredicatesExist; } - public Collection extractHiddenPredicates(AlternativeReqPredicate predicate) { - Collection result = new HashSet<>(); + public Collection extractHiddenPredicates(AlternativeReqPredicate predicate) { + Collection result = new HashSet<>(); for (RequiredCrySLPredicate reqPred : predicate.getRelAlternatives()) { - Collection extractedPreds = extractHiddenPredicates(reqPred); + Collection extractedPreds = extractHiddenPredicates(reqPred); result.addAll(extractedPreds); } @@ -1103,31 +1103,31 @@ public Collection extractHiddenPredicates(AlternativeReqPredica return result; } - public Collection extractHiddenPredicates(RequiredCrySLPredicate predicate) { + public Collection extractHiddenPredicates(RequiredCrySLPredicate predicate) { return extractHiddenPredicates( predicate.getLocation(), predicate.getPred(), predicate.getParamIndex()); } - private Collection extractHiddenPredicates( + private Collection extractHiddenPredicates( Statement statement, CrySLPredicate predicate, int index) { - Collection result = new HashSet<>(); + Collection result = new HashSet<>(); if (!hiddenPredicates.containsKey(statement)) { return result; } - Collection> hiddenPreds = + Collection> hiddenPreds = hiddenPredicates.get(statement); - for (Map.Entry entry : hiddenPreds) { + for (Map.Entry entry : hiddenPreds) { if (entry.getValue() != index) { continue; } - HiddenPredicate hiddenPredicate = entry.getKey(); + UnEnsuredPredicate unEnsuredPredicate = entry.getKey(); - if (hiddenPredicate.getPredicate().equals(predicate) - && doPredsMatch(predicate, hiddenPredicate)) { - result.add(hiddenPredicate); + if (unEnsuredPredicate.getPredicate().equals(predicate) + && doPredsMatch(predicate, unEnsuredPredicate)) { + result.add(unEnsuredPredicate); } } diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/EnsuredCrySLPredicate.java b/CryptoAnalysis/src/main/java/crypto/analysis/EnsuredPredicate.java similarity index 77% rename from CryptoAnalysis/src/main/java/crypto/analysis/EnsuredCrySLPredicate.java rename to CryptoAnalysis/src/main/java/crypto/analysis/EnsuredPredicate.java index f47c82367..a266ef0c7 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/EnsuredCrySLPredicate.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/EnsuredPredicate.java @@ -5,9 +5,9 @@ import java.util.Collection; import java.util.Objects; -public class EnsuredCrySLPredicate extends AbstractPredicate { +public class EnsuredPredicate extends AbstractPredicate { - public EnsuredCrySLPredicate( + public EnsuredPredicate( CrySLPredicate predicate, Collection parametersToValues) { super(predicate, parametersToValues); } @@ -19,7 +19,7 @@ public int hashCode() { @Override public boolean equals(Object obj) { - return super.equals(obj) && obj instanceof EnsuredCrySLPredicate; + return super.equals(obj) && obj instanceof EnsuredPredicate; } @Override diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/PredicateHandler.java b/CryptoAnalysis/src/main/java/crypto/analysis/PredicateHandler.java index 83ad319b5..9d358a12e 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/PredicateHandler.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/PredicateHandler.java @@ -68,7 +68,7 @@ private void collectMissingRequiredPredicates() { Collection violatedReqPreds = seed.computeViolatedRequiredPredicates(); for (RequiredCrySLPredicate reqPred : violatedReqPreds) { - Collection hiddenPreds = seed.extractHiddenPredicates(reqPred); + Collection hiddenPreds = seed.extractHiddenPredicates(reqPred); RequiredPredicateError reqPredError = new RequiredPredicateError(seed, reqPred, hiddenPreds); @@ -78,7 +78,7 @@ private void collectMissingRequiredPredicates() { Collection violatedAltPreds = seed.computeViolatedAlternativePredicates(); for (AlternativeReqPredicate altPred : violatedAltPreds) { - Collection hiddenPreds = seed.extractHiddenPredicates(altPred); + Collection hiddenPreds = seed.extractHiddenPredicates(altPred); AlternativeReqPredicateError reqPredError = new AlternativeReqPredicateError(seed, altPred, hiddenPreds); @@ -103,9 +103,9 @@ private void connectSubsequentErrors() { Collection errors = requiredPredicateErrors.get(seed); for (AbstractRequiredPredicateError error : errors) { - for (HiddenPredicate hiddenPredicate : error.getHiddenPredicates()) { + for (UnEnsuredPredicate unEnsuredPredicate : error.getHiddenPredicates()) { Collection precedingErrors = - hiddenPredicate.getPrecedingErrors(); + unEnsuredPredicate.getPrecedingErrors(); precedingErrors.forEach(error::addPrecedingError); precedingErrors.forEach(e -> e.addSubsequentError(error)); diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/HiddenPredicate.java b/CryptoAnalysis/src/main/java/crypto/analysis/UnEnsuredPredicate.java similarity index 95% rename from CryptoAnalysis/src/main/java/crypto/analysis/HiddenPredicate.java rename to CryptoAnalysis/src/main/java/crypto/analysis/UnEnsuredPredicate.java index 8dd0cf2bb..235608ce5 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/HiddenPredicate.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/UnEnsuredPredicate.java @@ -12,7 +12,7 @@ import java.util.Objects; import java.util.Set; -public class HiddenPredicate extends AbstractPredicate { +public class UnEnsuredPredicate extends AbstractPredicate { private final AnalysisSeedWithSpecification generatingSeed; private final Collection violations; @@ -25,7 +25,7 @@ public enum Violations { GeneratingStateIsNeverReached } - public HiddenPredicate( + public UnEnsuredPredicate( CrySLPredicate predicate, Collection parametersToValues, AnalysisSeedWithSpecification generatingSeed, @@ -89,7 +89,7 @@ public int hashCode() { @Override public boolean equals(Object obj) { return super.equals(obj) - && obj instanceof HiddenPredicate other + && obj instanceof UnEnsuredPredicate other && Objects.equals(generatingSeed, other.getGeneratingSeed()) && Objects.equals(violations, other.getViolations()); } diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiredPredicateError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiredPredicateError.java index 7020cfe94..8d95c1720 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiredPredicateError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiredPredicateError.java @@ -1,7 +1,7 @@ package crypto.analysis.errors; import boomerang.scene.Statement; -import crypto.analysis.HiddenPredicate; +import crypto.analysis.UnEnsuredPredicate; import crypto.analysis.IAnalysisSeed; import crysl.rule.CrySLRule; import java.util.Collection; @@ -9,26 +9,26 @@ import java.util.Set; /** - * Super class for errors that work with a {@link HiddenPredicate}. Currently, there are {@link + * Super class for errors that work with a {@link UnEnsuredPredicate}. Currently, there are {@link * RequiredPredicateError} that hold errors with single violated predicates and {@link * AlternativeReqPredicateError} that hold errors for violated predicates with alternatives. */ public abstract class AbstractRequiredPredicateError extends AbstractConstraintsError { - private final Collection hiddenPredicates; + private final Collection unEnsuredPredicates; public AbstractRequiredPredicateError( IAnalysisSeed seed, Statement errorStmt, CrySLRule rule, - Collection hiddenPredicates) { + Collection unEnsuredPredicates) { super(seed, errorStmt, rule); - this.hiddenPredicates = Set.copyOf(hiddenPredicates); + this.unEnsuredPredicates = Set.copyOf(unEnsuredPredicates); } - public Collection getHiddenPredicates() { - return hiddenPredicates; + public Collection getHiddenPredicates() { + return unEnsuredPredicates; } protected String getParamIndexAsText(int paramIndex) { @@ -46,13 +46,13 @@ protected String getParamIndexAsText(int paramIndex) { @Override public int hashCode() { - return Objects.hash(super.hashCode(), hiddenPredicates); + return Objects.hash(super.hashCode(), unEnsuredPredicates); } @Override public boolean equals(Object obj) { return super.equals(obj) && obj instanceof AbstractRequiredPredicateError other - && Objects.equals(hiddenPredicates, other.getHiddenPredicates()); + && Objects.equals(unEnsuredPredicates, other.getHiddenPredicates()); } } diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AlternativeReqPredicateError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AlternativeReqPredicateError.java index 36518364f..5c2a1c0cf 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AlternativeReqPredicateError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AlternativeReqPredicateError.java @@ -2,7 +2,7 @@ import crypto.analysis.AlternativeReqPredicate; import crypto.analysis.AnalysisSeedWithSpecification; -import crypto.analysis.HiddenPredicate; +import crypto.analysis.UnEnsuredPredicate; import crypto.analysis.RequiredCrySLPredicate; import crysl.rule.CrySLPredicate; import java.util.ArrayList; @@ -32,8 +32,8 @@ public class AlternativeReqPredicateError extends AbstractRequiredPredicateError public AlternativeReqPredicateError( AnalysisSeedWithSpecification seed, AlternativeReqPredicate violatedPred, - Collection hiddenPredicates) { - super(seed, violatedPred.getLocation(), seed.getSpecification(), hiddenPredicates); + Collection unEnsuredPredicates) { + super(seed, violatedPred.getLocation(), seed.getSpecification(), unEnsuredPredicates); this.contradictedPredicate = List.copyOf(violatedPred.getAllAlternatives()); this.relevantPredicates = Set.copyOf(violatedPred.getRelAlternatives()); diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java index 5f120fed0..e5f04179e 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java @@ -1,7 +1,7 @@ package crypto.analysis.errors; import crypto.analysis.AnalysisSeedWithSpecification; -import crypto.analysis.HiddenPredicate; +import crypto.analysis.UnEnsuredPredicate; import crypto.analysis.RequiredCrySLPredicate; import crysl.rule.CrySLPredicate; import java.util.Collection; @@ -26,8 +26,8 @@ public class RequiredPredicateError extends AbstractRequiredPredicateError { public RequiredPredicateError( AnalysisSeedWithSpecification seed, RequiredCrySLPredicate violatedPred, - Collection hiddenPredicates) { - super(seed, violatedPred.getLocation(), seed.getSpecification(), hiddenPredicates); + Collection unEnsuredPredicates) { + super(seed, violatedPred.getLocation(), seed.getSpecification(), unEnsuredPredicates); this.contradictedPredicate = violatedPred.getPred(); this.paramIndex = violatedPred.getParamIndex(); diff --git a/CryptoAnalysis/src/main/java/crypto/listener/AnalysisReporter.java b/CryptoAnalysis/src/main/java/crypto/listener/AnalysisReporter.java index 73a796f44..aafd27500 100644 --- a/CryptoAnalysis/src/main/java/crypto/listener/AnalysisReporter.java +++ b/CryptoAnalysis/src/main/java/crypto/listener/AnalysisReporter.java @@ -7,7 +7,7 @@ import boomerang.scene.Val; import com.google.common.collect.Multimap; import crypto.analysis.AbstractPredicate; -import crypto.analysis.EnsuredCrySLPredicate; +import crypto.analysis.EnsuredPredicate; import crypto.analysis.IAnalysisSeed; import crypto.analysis.errors.AbstractError; import crypto.analysis.errors.AlternativeReqPredicateError; @@ -224,7 +224,7 @@ public void checkedConstraints( public void ensuredPredicates( IAnalysisSeed seed, - Multimap> predicates) { + Multimap> predicates) { for (IResultsListener listener : resultsListeners) { listener.ensuredPredicates(seed, predicates); } diff --git a/CryptoAnalysis/src/main/java/crypto/listener/IResultsListener.java b/CryptoAnalysis/src/main/java/crypto/listener/IResultsListener.java index e49ee7fd8..b77023344 100644 --- a/CryptoAnalysis/src/main/java/crypto/listener/IResultsListener.java +++ b/CryptoAnalysis/src/main/java/crypto/listener/IResultsListener.java @@ -6,7 +6,7 @@ import boomerang.scene.Statement; import com.google.common.collect.Multimap; import crypto.analysis.AbstractPredicate; -import crypto.analysis.EnsuredCrySLPredicate; +import crypto.analysis.EnsuredPredicate; import crypto.analysis.IAnalysisSeed; import crypto.analysis.errors.AbstractError; import crypto.extractparameter.CallSiteWithExtractedValue; @@ -43,5 +43,5 @@ void generatedPredicate( void ensuredPredicates( IAnalysisSeed seed, - Multimap> predicates); + Multimap> predicates); } diff --git a/CryptoAnalysis/src/test/java/test/UsagePatternResultsListener.java b/CryptoAnalysis/src/test/java/test/UsagePatternResultsListener.java index f46171d4a..fd444d2af 100644 --- a/CryptoAnalysis/src/test/java/test/UsagePatternResultsListener.java +++ b/CryptoAnalysis/src/test/java/test/UsagePatternResultsListener.java @@ -10,7 +10,7 @@ import com.google.common.collect.Multimap; import com.google.common.collect.Table; import crypto.analysis.AbstractPredicate; -import crypto.analysis.EnsuredCrySLPredicate; +import crypto.analysis.EnsuredPredicate; import crypto.analysis.IAnalysisSeed; import crypto.analysis.errors.AbstractError; import crypto.extractparameter.CallSiteWithExtractedValue; @@ -156,7 +156,7 @@ public void generatedPredicate( @Override public void ensuredPredicates( IAnalysisSeed seed, - Multimap> predicates) { + Multimap> predicates) { for (Assertion a : assertions) { if (a instanceof HasEnsuredPredicateAssertion) { HasEnsuredPredicateAssertion assertion = (HasEnsuredPredicateAssertion) a; @@ -167,10 +167,10 @@ public void ensuredPredicates( Collection values = seed.getAnalysisResults().asStatementValWeightTable().columnKeySet(); - Collection> ensuredPreds = + Collection> ensuredPreds = predicates.get(assertion.getStmt()); - for (Map.Entry ensPred : ensuredPreds) { + for (Map.Entry ensPred : ensuredPreds) { assertion.reported(values, ensPred.getKey()); } } @@ -184,10 +184,10 @@ public void ensuredPredicates( Collection values = seed.getAnalysisResults().asStatementValWeightTable().columnKeySet(); - Collection> ensuredPreds = + Collection> ensuredPreds = predicates.get(assertion.getStmt()); - for (Map.Entry ensPred : ensuredPreds) { + for (Map.Entry ensPred : ensuredPreds) { assertion.reported(values, ensPred.getKey()); } } diff --git a/CryptoAnalysis/src/test/java/test/assertions/HasEnsuredPredicateAssertion.java b/CryptoAnalysis/src/test/java/test/assertions/HasEnsuredPredicateAssertion.java index b2240fe61..39aab9e75 100644 --- a/CryptoAnalysis/src/test/java/test/assertions/HasEnsuredPredicateAssertion.java +++ b/CryptoAnalysis/src/test/java/test/assertions/HasEnsuredPredicateAssertion.java @@ -3,7 +3,7 @@ import boomerang.scene.Statement; import boomerang.scene.Val; import crypto.analysis.AbstractPredicate; -import crypto.analysis.HiddenPredicate; +import crypto.analysis.UnEnsuredPredicate; import java.util.Collection; import test.Assertion; @@ -39,7 +39,7 @@ public Statement getStmt() { } public void reported(Collection seed, AbstractPredicate pred) { - if (!seed.contains(val) || pred instanceof HiddenPredicate) { + if (!seed.contains(val) || pred instanceof UnEnsuredPredicate) { return; } diff --git a/CryptoAnalysis/src/test/java/test/assertions/HasGeneratedPredicateAssertion.java b/CryptoAnalysis/src/test/java/test/assertions/HasGeneratedPredicateAssertion.java index b842f60f2..7e8406219 100644 --- a/CryptoAnalysis/src/test/java/test/assertions/HasGeneratedPredicateAssertion.java +++ b/CryptoAnalysis/src/test/java/test/assertions/HasGeneratedPredicateAssertion.java @@ -3,7 +3,7 @@ import boomerang.scene.Statement; import boomerang.scene.Val; import crypto.analysis.AbstractPredicate; -import crypto.analysis.HiddenPredicate; +import crypto.analysis.UnEnsuredPredicate; import java.util.Collection; import test.Assertion; @@ -29,7 +29,7 @@ public boolean isImprecise() { } public void reported(Collection seed, AbstractPredicate predicate) { - if (seed.contains(val) && !(predicate instanceof HiddenPredicate)) { + if (seed.contains(val) && !(predicate instanceof UnEnsuredPredicate)) { satisfied = true; } } diff --git a/CryptoAnalysis/src/test/java/test/assertions/HasNotGeneratedPredicateAssertion.java b/CryptoAnalysis/src/test/java/test/assertions/HasNotGeneratedPredicateAssertion.java index 88cf79668..7d6e28785 100644 --- a/CryptoAnalysis/src/test/java/test/assertions/HasNotGeneratedPredicateAssertion.java +++ b/CryptoAnalysis/src/test/java/test/assertions/HasNotGeneratedPredicateAssertion.java @@ -3,7 +3,7 @@ import boomerang.scene.Statement; import boomerang.scene.Val; import crypto.analysis.AbstractPredicate; -import crypto.analysis.HiddenPredicate; +import crypto.analysis.UnEnsuredPredicate; import java.util.Collection; import test.Assertion; @@ -33,7 +33,7 @@ public Statement getStatement() { } public void reported(Collection seed, AbstractPredicate predicate) { - if (seed.contains(val) && !(predicate instanceof HiddenPredicate)) { + if (seed.contains(val) && !(predicate instanceof UnEnsuredPredicate)) { imprecise = true; } } diff --git a/CryptoAnalysis/src/test/java/test/assertions/NotHasEnsuredPredicateAssertion.java b/CryptoAnalysis/src/test/java/test/assertions/NotHasEnsuredPredicateAssertion.java index 79e6f5696..5b71ebb53 100644 --- a/CryptoAnalysis/src/test/java/test/assertions/NotHasEnsuredPredicateAssertion.java +++ b/CryptoAnalysis/src/test/java/test/assertions/NotHasEnsuredPredicateAssertion.java @@ -3,7 +3,7 @@ import boomerang.scene.Statement; import boomerang.scene.Val; import crypto.analysis.AbstractPredicate; -import crypto.analysis.HiddenPredicate; +import crypto.analysis.UnEnsuredPredicate; import java.util.Collection; import test.Assertion; @@ -39,7 +39,7 @@ public Statement getStmt() { } public void reported(Collection seed, AbstractPredicate pred) { - if (!seed.contains(val) || pred instanceof HiddenPredicate) { + if (!seed.contains(val) || pred instanceof UnEnsuredPredicate) { return; } From fdf558eecdbca41f0ee1c12a0521e2b4281ca0b6 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Fri, 20 Dec 2024 14:56:49 +0100 Subject: [PATCH 28/32] Add some comments for subsequent errors --- .../crypto/analysis/AbstractPredicate.java | 6 +++ .../AnalysisSeedWithSpecification.java | 17 +++++-- .../crypto/analysis/EnsuredPredicate.java | 5 ++ .../crypto/analysis/UnEnsuredPredicate.java | 46 +++++++++++++++++++ .../AbstractRequiredPredicateError.java | 2 +- .../errors/AlternativeReqPredicateError.java | 2 +- .../errors/RequiredPredicateError.java | 2 +- 7 files changed, 74 insertions(+), 6 deletions(-) diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/AbstractPredicate.java b/CryptoAnalysis/src/main/java/crypto/analysis/AbstractPredicate.java index 47ae33dc8..fd3c20082 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/AbstractPredicate.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/AbstractPredicate.java @@ -5,6 +5,12 @@ import java.util.Collection; import java.util.Objects; +/** + * Super class for predicates that are propagated during the analysis. Each predicate is either an + * {@link EnsuredPredicate} or an {@link UnEnsuredPredicate}. The former ones keep track of all + * predicates from the ENSURES section where no violations from a rule are found. The latter ones + * are propagated to keep track of predicates and seeds if there is a violation. + */ public abstract class AbstractPredicate { private final CrySLPredicate predicate; diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java b/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java index 78ae3adfb..8f961dbcc 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/AnalysisSeedWithSpecification.java @@ -552,7 +552,14 @@ private Collection computeGeneratedParameterSeeds( return result; } + /** + * Ensure the predicates from the ENSURES section and transfer them to this seed or other seeds. + * If there are no violations for the rule, propagate an {@link EnsuredPredicate}. Otherwise, if + * there is at least one violation, collect them and propagate a corresponding {@link + * UnEnsuredPredicate}. + */ public void ensurePredicates() { + // Check whether all constraints from the CONSTRAINTS and REQUIRES section is satisfied boolean satisfiesConstraintSystem = isConstraintSystemSatisfied(); Collection expectedPredStatements = expectedPredicates.keySet(); @@ -566,6 +573,7 @@ public void ensurePredicates() { for (CrySLPredicate predToBeEnsured : predsToBeEnsured) { Collection violations = new HashSet<>(); + // Check whether there is a ForbiddenMethodError from previous checks if (errorCollection.stream().anyMatch(e -> e instanceof ForbiddenMethodError)) { violations.add(UnEnsuredPredicate.Violations.CallToForbiddenMethod); } @@ -574,6 +582,7 @@ public void ensurePredicates() { violations.add(UnEnsuredPredicate.Violations.ConstraintsAreNotSatisfied); } + // Check whether there is a predicate condition and whether it is satisfied if (predToBeEnsured.getConstraint().isPresent() && isPredConditionViolated(predToBeEnsured)) { violations.add(UnEnsuredPredicate.Violations.ConditionIsNotSatisfied); @@ -609,7 +618,7 @@ && isPredConditionViolated(predToBeEnsured)) { * single call to a method, e.g. Object o = new Object(); o.m();. After * the call to m1(), o is always in state 0 and 1, although it should only be 1 */ - // allViolations.add(HiddenPredicate.Violations.GeneratingStateMayNotBeReached); + // allViolations.add(UnEnsuredPredicate.Violations.GeneratingStateMayNotBeReached); } AbstractPredicate generatedPred; @@ -1091,7 +1100,8 @@ private boolean doPredsMatch(CrySLPredicate pred, AbstractPredicate ensPred) { return requiredPredicatesExist; } - public Collection extractHiddenPredicates(AlternativeReqPredicate predicate) { + public Collection extractHiddenPredicates( + AlternativeReqPredicate predicate) { Collection result = new HashSet<>(); for (RequiredCrySLPredicate reqPred : predicate.getRelAlternatives()) { @@ -1103,7 +1113,8 @@ public Collection extractHiddenPredicates(AlternativeReqPred return result; } - public Collection extractHiddenPredicates(RequiredCrySLPredicate predicate) { + public Collection extractHiddenPredicates( + RequiredCrySLPredicate predicate) { return extractHiddenPredicates( predicate.getLocation(), predicate.getPred(), predicate.getParamIndex()); } diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/EnsuredPredicate.java b/CryptoAnalysis/src/main/java/crypto/analysis/EnsuredPredicate.java index a266ef0c7..c1f23c1b6 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/EnsuredPredicate.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/EnsuredPredicate.java @@ -5,6 +5,11 @@ import java.util.Collection; import java.util.Objects; +/** + * Wrapper class for a single {@link CrySLPredicate} to keep track of ensured predicate during the + * analysis. A predicate is only ensured if there are no violations for a corresponding rule. + * Otherwise, the analysis propagates an {@link UnEnsuredPredicate}. + */ public class EnsuredPredicate extends AbstractPredicate { public EnsuredPredicate( diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/UnEnsuredPredicate.java b/CryptoAnalysis/src/main/java/crypto/analysis/UnEnsuredPredicate.java index 235608ce5..ca8a6d60a 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/UnEnsuredPredicate.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/UnEnsuredPredicate.java @@ -12,16 +12,47 @@ import java.util.Objects; import java.util.Set; +/** + * Wrapper class for a single {@link CrySLPredicate} that could not be ensured during the analysis. + * If a seed cannot generate a predicate due to some violations from its CrySL rule, the analysis + * keeps track of those violations and the seed s.t. other seeds can reason about why the predicate + * was not ensured. This way, the analysis can connect corresponding subsequent errors. + * + *

See the paper + */ public class UnEnsuredPredicate extends AbstractPredicate { private final AnalysisSeedWithSpecification generatingSeed; private final Collection violations; + /** Collection of violations that may cause a predicate to be not ensured */ public enum Violations { + /** Violation if there is a call to a method from the FORBIDDEN section. */ CallToForbiddenMethod, + + /** + * Violation if there is an unsatisfied constraint. Constraints include basic constraints + * from the CONSTRAINTS section and required predicates from the REQUIRES section. + */ ConstraintsAreNotSatisfied, + + /** + * Violation if the condition of predicate from the ENSURES section is not satisfied. Note + * that the analysis does not report an error because the condition is not required to be + * satisfied. + */ ConditionIsNotSatisfied, + + /** + * Violation if there are dataflow paths where the seed does not reach an accepting state to + * generate a predicate. + */ GeneratingStateMayNotBeReached, + + /** + * Violation if there is no dataflow path where the seed reaches an accepting state to + * generate a predicate. + */ GeneratingStateIsNeverReached } @@ -44,9 +75,15 @@ public Collection getViolations() { return violations; } + /** + * Compute the preceding errors that cause the predicate stored in this class to be not ensured. + * + * @return the errors that cause the predicate to be not ensured + */ public Collection getPrecedingErrors() { Collection result = new HashSet<>(); + // Collect all ForbiddenMethodErrors if (violations.contains(Violations.CallToForbiddenMethod)) { Collection forbiddenMethodErrors = generatingSeed.getErrors().stream() @@ -55,6 +92,9 @@ public Collection getPrecedingErrors() { result.addAll(forbiddenMethodErrors); } + /* Collect the ConstraintErrors. This includes error violating constraints from the + * CONSTRAINTS section and violated predicates from the REQUIRES section. + */ if (violations.contains(Violations.ConstraintsAreNotSatisfied)) { Collection constraintErrors = generatingSeed.getErrors().stream() @@ -63,12 +103,18 @@ public Collection getPrecedingErrors() { result.addAll(constraintErrors); } + /* If the predicate condition in the ENSURES section is not satisfied, a + * PredicateConstraintError is created. Note that these errors are not reported since + * they are not required to be satisfied. However, a corresponding error indicate that + * the condition is not satisfied. + */ if (violations.contains(Violations.ConditionIsNotSatisfied)) { PredicateConstraintError error = new PredicateConstraintError(generatingSeed, getPredicate()); result.add(error); } + // Collect all errors that cause the seed to not reach an accepting state if (violations.contains(Violations.GeneratingStateMayNotBeReached) || violations.contains(Violations.GeneratingStateIsNeverReached)) { Collection orderError = diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiredPredicateError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiredPredicateError.java index 8d95c1720..8b899ab63 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiredPredicateError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AbstractRequiredPredicateError.java @@ -1,8 +1,8 @@ package crypto.analysis.errors; import boomerang.scene.Statement; -import crypto.analysis.UnEnsuredPredicate; import crypto.analysis.IAnalysisSeed; +import crypto.analysis.UnEnsuredPredicate; import crysl.rule.CrySLRule; import java.util.Collection; import java.util.Objects; diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AlternativeReqPredicateError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AlternativeReqPredicateError.java index 5c2a1c0cf..eedff51e5 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/AlternativeReqPredicateError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/AlternativeReqPredicateError.java @@ -2,8 +2,8 @@ import crypto.analysis.AlternativeReqPredicate; import crypto.analysis.AnalysisSeedWithSpecification; -import crypto.analysis.UnEnsuredPredicate; import crypto.analysis.RequiredCrySLPredicate; +import crypto.analysis.UnEnsuredPredicate; import crysl.rule.CrySLPredicate; import java.util.ArrayList; import java.util.Collection; diff --git a/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java b/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java index e5f04179e..8fcaa8386 100644 --- a/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java +++ b/CryptoAnalysis/src/main/java/crypto/analysis/errors/RequiredPredicateError.java @@ -1,8 +1,8 @@ package crypto.analysis.errors; import crypto.analysis.AnalysisSeedWithSpecification; -import crypto.analysis.UnEnsuredPredicate; import crypto.analysis.RequiredCrySLPredicate; +import crypto.analysis.UnEnsuredPredicate; import crysl.rule.CrySLPredicate; import java.util.Collection; import java.util.Objects; From d0124aa79ff8232088749a391e13b52bf09c5291 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Fri, 20 Dec 2024 15:49:46 +0100 Subject: [PATCH 29/32] Update README --- README.md | 78 +++++++++++++++++++++++++++++------------ misc/visualization.png | Bin 0 -> 69687 bytes 2 files changed, 55 insertions(+), 23 deletions(-) create mode 100644 misc/visualization.png diff --git a/README.md b/README.md index ad3e9dfdb..6f45d3e8d 100644 --- a/README.md +++ b/README.md @@ -17,25 +17,19 @@ We further provide two SAST tools that allow the analysis of Java and Android ap ## Releases -You can checkout a pre-compiled version of CogniCryptSAST [here](https://github.com/CROSSINGTUD/CryptoAnalysis/releases). We recommend using the latest version. - -Download the two files: -* CryptoAnalysis-x.y.z-jar-with-dependencies.jar -* JCA-CrySL-rules.zip - -You can find CogniCryptSAST also on [Maven Central](https://central.sonatype.com/artifact/de.fraunhofer.iem/CryptoAnalysis). +You can checkout a pre-compiled version of CogniCryptSAST [here](https://github.com/CROSSINGTUD/CryptoAnalysis/releases). We recommend using the latest version. You can find CogniCryptSAST also on [Maven Central](https://central.sonatype.com/artifact/de.fraunhofer.iem/CryptoAnalysis). ## Checkout and Build CogniCryptSAST uses Maven as build tool. You can compile and build this project via -```mvn clean package -DskipTests=true```. +```mvn clean package -DskipTests```. The packaged `jar` artifacts including all dependencies can be found in `/apps`. Building requires at least Java 17. ## CogniCryptSAST for Java Applications -CogniCryptSAST can be started in headless mode (i.e., detached from Eclipse) via the file `HeadlessJavaScanner-x.y.z-jar-with-dependencies.jar`. It requires two arguments: +CogniCryptSAST can be started in headless mode as CLI tool via the file `HeadlessJavaScanner-x.y.z-jar-with-dependencies.jar`. It requires two arguments: * The path to the directory of the CrySL (source code format) rule files. The source code for the rules which contain specification for the JCA is found [here](https://github.com/CROSSINGTUD/Crypto-API-Rules). * The path of the application to be analyzed (.jar file or the root compilation output folder which contains the .class files in subdirectories) @@ -45,13 +39,7 @@ java -jar HeadlessJavaScanner-x.y.z-jar-with-dependencies.jar --appPath ``` -For an easy start we prepared a .jar containing classes with crypto misuses. The source code for these misuses is found [here](https://github.com/CROSSINGTUD/CryptoAnalysis/tree/develop/CryptoAnalysisTargets/CogniCryptDemoExample/src/main/java/example). To run CogniCryptSAST on these classes, simply execute the following command. - -``` -java -jar HeadlessJavaScanner-x.y.z-jar-with-dependencies.jar - --rulesDir $(pwd)/CryptoAnalysis-Core/src/main/resources/JavaCryptographicArchitecture - --appPath $(pwd)/CryptoAnalysisTargets/CogniCryptDemoExample/Examples.jar -``` +For an easy start we prepared a .jar containing classes with crypto misuses. The source code for these misuses is found [here](https://github.com/CROSSINGTUD/CryptoAnalysis/tree/develop/CryptoAnalysisTargets/CogniCryptDemoExample/src/main/java/example). Other additional arguments that can be used are as follows: @@ -113,12 +101,7 @@ CogniCryptSAST supports different report formats, which can be set by - `CSV_SUMMARY`: The report is written to the file `CryptoAnalysis-Report-Summary.csv` and contains a summary of the analysis results. Compared to the `CSV` format, this format does not provide concrete information about the errors, it only lists the amount of each misuse type. This option was previously implemented by the `CSV` option, which has been changed to provide more detailed information about the errors in the CSV format. - `GITHUB_ANNOTATION`: Works like `CMD` but also outputs all violations as annotations when running inside as a GitHub Action. -If the `--reportformat` option is not specified, CogniCryptSAST defaults to the `CMD` option. It also allows the usage of multiple different formats for the same analysis (e.g. `--reportformat CMD,TXT,CSV` creates a report, which is printed to the command line and is written to a text and CSV file). If the option `--reportPath ` is set, the reports are created in the specified directory. - -## Updating CrySL Rules - -The tool takes CrySL rules in their source code formats (crysl). You can adapt the rules in any text editor. -Additionaly, the [Eclipse plugin CogniCrypt](https://github.com/CROSSINGTUD/CogniCrypt) ships with a CrySL editor to modify the rules with IDE support (e.g., content assist, auto completion, etc.). A step-by-step-explanation on how edit CrySL rules is avialable at the tool's website [cognicrypt.org](https://www.eclipse.org/cognicrypt/documentation/crysl/). +If the `--reportformat` option is not specified, CogniCryptSAST defaults to the `CMD` option. It also allows the usage of multiple different formats for the same analysis (e.g. `--reportformat CMD,TXT,CSV` creates a report, which is printed to the command line and is written to a text and CSV file). If the option `--reportPath ` is set, the reports (and the visualization) are created in the specified directory. ## CogniCryptSAST for Android Applications @@ -131,7 +114,7 @@ CogniCryptSAST can also be run on Android Applications using the Andr ``` java -jar HeadlessAndroidScanner-x.y.z-jar-with-dependencies.jar --rulesDir - --platformDirectory + --platformDirectory --appPath ``` Optional parameters are `--reportPath` and `--reportFormat`. They have the same functionality as the `HeadlessJavaScanner-x.y.z-jar-with-dependencies.jar` (see above). @@ -145,3 +128,52 @@ We hare happy for every contribution from the community! * [Contributing](CONTRIBUTING.md) for details on issues and merge requests. * [Coding Guidles](CODING.md) for this project. + +## Running CognitCryptSAST + +Let's assume we have the following program with some violations: + +```java +import java.security.GeneralSecurityException; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import javax.crypto.Cipher; + +public class Example { + + public static void main(String[] args) throws GeneralSecurityException { + // Constraint Error: "DES" is not allowed + KeyGenerator generator = KeyGenerator.getInstance("DES"); // r0 + + // Constraint Error: Key size of 64 is not allowed + generator.init(64); + + // KeyGenerator is not correctly initialized + // RequiredPredicateEror: Generated key is not secure + SecretKey key = generator.generateKey(); // r1 + + // Constraint Error: "DES" is not allowed + Cipher cipher = Cipher.getInstance("DES"); // r2 + + // RequiredPredicateError: "key" is not securely generated + cipher.init(Cipher.ENCRYPT_MODE, key); + + // IncompleteOperationError: Cipher object is not used + } +} +``` + +Using the [JCA rules](https://github.com/CROSSINGTUD/Crypto-API-Rules/tree/master/JavaCryptographicArchitecture/src), we execute the following command on a compiled version of this program: + +```bash +java -jar HeadlessJavaScanner-x.y.z-jar-with-dependencies.jar --appPath ./Examples.jar --rulesDir ./JCA-CrySL-rules.zip --reportFormat CMD --reportPath ./output/ --visualization +``` + +CogniCryptSAST runs the analysis and prints a report to the command line. In total, it reports 3 `ConstraintErrors`, 2 `RequiredPredicateErrors` and 1 `IncompleteOperationError`, and their positions in the original programs. Additionally, since we use `--visualization`, it creates the following image `visualization.png` in the directory `./output/`: + +

+ +

+ +You can see that two `ConstraintErrors` on the object `r0` (KeyGenerator) cause a `RequiredPredicateError` on the object `r1` (SecretKey) which in turn causes a `RequiredPredicateError` on the object `r2` (Cipher). Additionally, there is another `ConstraintError` and `IncompleteOperationError` on the Cipher object. Note that the variables and statements correspond to the intermediate representation Jimple. You can match the variables to the command line output that lists all analyzed objects. diff --git a/misc/visualization.png b/misc/visualization.png new file mode 100644 index 0000000000000000000000000000000000000000..006d21fbd4ef5a40c6b86e909b6a0a8bf3adcf1e GIT binary patch literal 69687 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

|bWcUPUR>jU-2c}4IL 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

(wsBX}_#k;M(INn;h}xk{sx67b5(FwT4tZ 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 Date: Fri, 20 Dec 2024 15:51:33 +0100 Subject: [PATCH 30/32] Update README.md --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6f45d3e8d..880166f5f 100644 --- a/README.md +++ b/README.md @@ -170,10 +170,8 @@ Using the [JCA rules](https://github.com/CROSSINGTUD/Crypto-API-Rules/tree/maste java -jar HeadlessJavaScanner-x.y.z-jar-with-dependencies.jar --appPath ./Examples.jar --rulesDir ./JCA-CrySL-rules.zip --reportFormat CMD --reportPath ./output/ --visualization ``` -CogniCryptSAST runs the analysis and prints a report to the command line. In total, it reports 3 `ConstraintErrors`, 2 `RequiredPredicateErrors` and 1 `IncompleteOperationError`, and their positions in the original programs. Additionally, since we use `--visualization`, it creates the following image `visualization.png` in the directory `./output/`: +CogniCryptSAST runs the analysis and prints a report to the command line. In total, it reports 3 `ConstraintErrors`, 2 `RequiredPredicateErrors` and 1 `IncompleteOperationError`, and their positions in the original programs. Additionally, since we use `--visualization`, it creates the following image `visualization.png` in the directory `./output/`: -

- -

+![visualization.png](misc/visualization.png) You can see that two `ConstraintErrors` on the object `r0` (KeyGenerator) cause a `RequiredPredicateError` on the object `r1` (SecretKey) which in turn causes a `RequiredPredicateError` on the object `r2` (Cipher). Additionally, there is another `ConstraintError` and `IncompleteOperationError` on the Cipher object. Note that the variables and statements correspond to the intermediate representation Jimple. You can match the variables to the command line output that lists all analyzed objects. From 87c142ccf04846051e13285ac3c918c3242ea3a9 Mon Sep 17 00:00:00 2001 From: Sven Meyer Date: Fri, 20 Dec 2024 15:52:42 +0100 Subject: [PATCH 31/32] Update visualization.png --- misc/visualization.png | Bin 69687 -> 78039 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/misc/visualization.png b/misc/visualization.png index 006d21fbd4ef5a40c6b86e909b6a0a8bf3adcf1e..bcec9a00b4f19367b6d24621e2c4c821e7c1f837 100644 GIT binary patch literal 78039 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

26fg`u6{d+dB{D+dktCyJk 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

|bWcUPUR>jU-2c}4IL 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

(wsBX}_#k;M(INn;h}xk{sx67b5(FwT4tZ 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 Date: Fri, 20 Dec 2024 16:40:24 +0100 Subject: [PATCH 32/32] Update README.md --- README.md | 94 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 880166f5f..d028b2f63 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,54 @@ The static analysis CogniCryptSAST takes rules written in the specifi and performs a static analysis based on the specification of the rules. CrySL is a domain-specific language (DSL) designed to encode usage specifications for cryptographic libaries (e.g., the [JCA](https://docs.oracle.com/en/java/javase/14/security/java-cryptography-architecture-jca-reference-guide.html) in particular). More information on CrySL and the static analysis may be found in [this paper](http://drops.dagstuhl.de/opus/volltexte/2018/9215/). +## Running CognitCryptSAST + +Let's assume we have the following program with some violations: + +```java +import java.security.GeneralSecurityException; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import javax.crypto.Cipher; + +public class Example { + + public static void main(String[] args) throws GeneralSecurityException { + // Constraint Error: "DES" is not allowed + KeyGenerator generator = KeyGenerator.getInstance("DES"); // r0 + + // Constraint Error: Key size of 64 is not allowed + generator.init(64); + + // KeyGenerator is not correctly initialized + // RequiredPredicateEror: Generated key is not secure + SecretKey key = generator.generateKey(); // r1 + + // Constraint Error: "DES" is not allowed + Cipher cipher = Cipher.getInstance("DES"); // r2 + + // RequiredPredicateError: "key" is not securely generated + cipher.init(Cipher.ENCRYPT_MODE, key); + + // IncompleteOperationError: Cipher object is not used + } +} +``` + +Using the [JCA rules](https://github.com/CROSSINGTUD/Crypto-API-Rules/tree/master/JavaCryptographicArchitecture/src), we execute the following command on a compiled version of this program: + +```bash +java -jar HeadlessJavaScanner-x.y.z-jar-with-dependencies.jar --appPath ./Examples.jar --rulesDir ./JCA-CrySL-rules.zip --reportFormat CMD --reportPath ./output/ --visualization +``` + +CogniCryptSAST runs the analysis and prints a report to the command line. In total, it reports 3 `ConstraintErrors`, 2 `RequiredPredicateErrors` and 1 `IncompleteOperationError`, and their positions in the original programs. Additionally, since we use `--visualization`, it creates the following image `visualization.png` in the directory `./output/`: + +![visualization.png](misc/visualization.png) + +You can see that two `ConstraintErrors` on the object `r0` (KeyGenerator) cause a `RequiredPredicateError` on the object `r1` (SecretKey) which in turn causes a `RequiredPredicateError` on the object `r2` (Cipher). Additionally, there is another `ConstraintError` and `IncompleteOperationError` on the Cipher object. Note that the variables and statements correspond to the intermediate representation Jimple. You can match the variables to the command line output that lists all analyzed objects. + + ## Structure We provide the implementation of the static analysis of CogniCrypt in: * `CryptoAnalysis` contains the components for the actual analysis @@ -129,49 +177,3 @@ We hare happy for every contribution from the community! * [Contributing](CONTRIBUTING.md) for details on issues and merge requests. * [Coding Guidles](CODING.md) for this project. -## Running CognitCryptSAST - -Let's assume we have the following program with some violations: - -```java -import java.security.GeneralSecurityException; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import javax.crypto.Cipher; - -public class Example { - - public static void main(String[] args) throws GeneralSecurityException { - // Constraint Error: "DES" is not allowed - KeyGenerator generator = KeyGenerator.getInstance("DES"); // r0 - - // Constraint Error: Key size of 64 is not allowed - generator.init(64); - - // KeyGenerator is not correctly initialized - // RequiredPredicateEror: Generated key is not secure - SecretKey key = generator.generateKey(); // r1 - - // Constraint Error: "DES" is not allowed - Cipher cipher = Cipher.getInstance("DES"); // r2 - - // RequiredPredicateError: "key" is not securely generated - cipher.init(Cipher.ENCRYPT_MODE, key); - - // IncompleteOperationError: Cipher object is not used - } -} -``` - -Using the [JCA rules](https://github.com/CROSSINGTUD/Crypto-API-Rules/tree/master/JavaCryptographicArchitecture/src), we execute the following command on a compiled version of this program: - -```bash -java -jar HeadlessJavaScanner-x.y.z-jar-with-dependencies.jar --appPath ./Examples.jar --rulesDir ./JCA-CrySL-rules.zip --reportFormat CMD --reportPath ./output/ --visualization -``` - -CogniCryptSAST runs the analysis and prints a report to the command line. In total, it reports 3 `ConstraintErrors`, 2 `RequiredPredicateErrors` and 1 `IncompleteOperationError`, and their positions in the original programs. Additionally, since we use `--visualization`, it creates the following image `visualization.png` in the directory `./output/`: - -![visualization.png](misc/visualization.png) - -You can see that two `ConstraintErrors` on the object `r0` (KeyGenerator) cause a `RequiredPredicateError` on the object `r1` (SecretKey) which in turn causes a `RequiredPredicateError` on the object `r2` (Cipher). Additionally, there is another `ConstraintError` and `IncompleteOperationError` on the Cipher object. Note that the variables and statements correspond to the intermediate representation Jimple. You can match the variables to the command line output that lists all analyzed objects.