Repository: prebid/prebid-server
Pull request #2905 (2023-07-05 21:21:07)

Bump google.golang.org/grpc from 1.46.2 to 1.53.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.46.2 to 1.53.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/grpc/grpc-go/releases">google.golang.org/grpc's releases</a>.</em></p>
<blockquote>
<h2>Release 1.53.0</h2>
<h1>API Changes</h1>
<ul>
<li>balancer: support injection of per-call metadata from LB policies (<a href="https://redirect.github.com/grpc/grpc-go/issues/5853">#5853</a>)</li>
<li>resolver: remove deprecated field <code>resolver.Target.Endpoint</code> and replace with <code>resolver.Target.Endpoint()</code> (<a href="https://redirect.github.com/grpc/grpc-go/issues/5852">#5852</a>)
<ul>
<li>Special Thanks: <a href="https://github.com/kylejb"><code>@​kylejb</code></a></li>
</ul>
</li>
</ul>
<h1>New Features</h1>
<ul>
<li>xds/ringhash: introduce <code>GRPC_RING_HASH_CAP</code> environment variable to override the maximum ring size. (<a href="https://redirect.github.com/grpc/grpc-go/issues/5884">#5884</a>)</li>
<li>rls: propagate headers received in RLS response to backends (<a href="https://redirect.github.com/grpc/grpc-go/issues/5883">#5883</a>)</li>
</ul>
<h1>Bug Fixes</h1>
<ul>
<li>transport: drain client transport when streamID approaches MaxStreamID (<a href="https://redirect.github.com/grpc/grpc-go/issues/5889">#5889</a>)</li>
<li>server: after GracefulStop, ensure connections are closed when final RPC completes (<a href="https://redirect.github.com/grpc/grpc-go/issues/5968">#5968</a>)</li>
<li>server: fix a few issues where grpc server uses RST_STREAM for non-HTTP/2 errors (<a href="https://redirect.github.com/grpc/grpc-go/issues/5893">#5893</a>)</li>
<li>xdsclient: fix race which can happen when multiple load reporting calls are made at the same time. (<a href="https://redirect.github.com/grpc/grpc-go/issues/5927">#5927</a>)</li>
<li>rls: fix a data race involving the LRU cache (<a href="https://redirect.github.com/grpc/grpc-go/issues/5925">#5925</a>)</li>
<li>xds: fix panic involving double close of channel in xDS transport (<a href="https://redirect.github.com/grpc/grpc-go/issues/5959">#5959</a>)</li>
<li>gcp/observability: update method name validation (<a href="https://redirect.github.com/grpc/grpc-go/issues/5951">#5951</a>)</li>
</ul>
<h1>Documentation</h1>
<ul>
<li>credentials/oauth: mark <code>NewOauthAccess</code> as deprecated (<a href="https://redirect.github.com/grpc/grpc-go/issues/5882">#5882</a>)
<ul>
<li>Special Thanks: <a href="https://github.com/buzzsurfr"><code>@​buzzsurfr</code></a></li>
</ul>
</li>
</ul>
<h2>Release 1.52.3</h2>
<h1>Bug Fixes</h1>
<ul>
<li>Fix user-agent version</li>
</ul>
<h2>Release 1.52.2</h2>
<h1>Bug Fixes</h1>
<ul>
<li>xds: fix panic involving double close of channel in xDS transport (<a href="https://redirect.github.com/grpc/grpc-go/issues/5959">#5959</a>)</li>
</ul>
<h2>Release 1.52.1</h2>
<h1>Bug Fixes</h1>
<ul>
<li>grpclb: rename grpclbstate package back to state (<a href="https://redirect.github.com/grpc/grpc-go/issues/5963">#5963</a>)</li>
</ul>
<h2>Release 1.52.0</h2>
<h1>New Features</h1>
<ul>
<li>xdsclient: log node ID with verbosity INFO (<a href="https://redirect.github.com/grpc/grpc-go/issues/5860">#5860</a>)</li>
<li>ringhash: impose cap on <code>max_ring_size</code> to reduce possibility of OOMs (<a href="https://redirect.github.com/grpc/grpc-go/issues/5801">#5801</a>)</li>
</ul>
<h1>Behavior Changes</h1>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/grpc/grpc-go/commit/dba26e15a07f43875ccf806a2dd6cbcbc1c12eab"><code>dba26e1</code></a> Change version to 1.53.0 (<a href="https://redirect.github.com/grpc/grpc-go/issues/5983">#5983</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/2a1e9348ff7b5d9f4b5039e84e6c9873b5b3e26e"><code>2a1e934</code></a> server: after GracefulStop, ensure connections are closed when final RPC comp...</li>
<li><a href="https://github.com/grpc/grpc-go/commit/e2d69aa076dd070e3668784c4dc8bcf7131b3f67"><code>e2d69aa</code></a> tests: fix spelling of variable (<a href="https://redirect.github.com/grpc/grpc-go/issues/5966">#5966</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/a6376c9893f56fc3819bee9ef5d71f55cc2d38dd"><code>a6376c9</code></a> xds/resolver: cleanup tests to use real xDS client 3/n (<a href="https://redirect.github.com/grpc/grpc-go/issues/5953">#5953</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/bf8fc46fa6eb913e4ed0f6dee6c6a7b75e85fbf0"><code>bf8fc46</code></a> xds/resolver: cleanup tests to use real xDS client 5/n (<a href="https://redirect.github.com/grpc/grpc-go/issues/5955">#5955</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/3930549b38c0fc4cd94a95efccf7cef5f90515fd"><code>3930549</code></a> resolver: replace resolver.Target.Endpoint field with Endpoint() method (<a href="https://redirect.github.com/grpc/grpc-go/issues/5852">#5852</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/894816c487f8dd48fc971c45a7c5baa4b86ef7de"><code>894816c</code></a> grpclb: rename <code>grpclbstate</code> package back to <code>state</code> (<a href="https://redirect.github.com/grpc/grpc-go/issues/5962">#5962</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/e5a0237a46a5f95fa571624929be10c7afebb180"><code>e5a0237</code></a> encoding: fix duplicate compressor names (<a href="https://redirect.github.com/grpc/grpc-go/issues/5958">#5958</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/4adb2a7a00d8b62df5ea34d520fe3ca13bffd31a"><code>4adb2a7</code></a> xds/resolver: cleanup tests to use real xDS client 2/n (<a href="https://redirect.github.com/grpc/grpc-go/issues/5952">#5952</a>)</li>
<li><a href="https://github.com/grpc/grpc-go/commit/52a8392f374b8cd60e176b67925a7f8c1605d014"><code>52a8392</code></a> gcp/observability: update method name validation (<a href="https://redirect.github.com/grpc/grpc-go/issues/5951">#5951</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/grpc/grpc-go/compare/v1.46.2...v1.53.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=google.golang.org/grpc&package-manager=go_modules&previous-version=1.46.2&new-version=1.53.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/prebid/prebid-server/network/alerts).

</details>


Patch:
diff --git a/.github/workflows/helpers/pull-request-utils.js b/.github/workflows/helpers/pull-request-utils.js
new file mode 100644
index 00000000..64ea5382
--- /dev/null
+++ b/.github/workflows/helpers/pull-request-utils.js
@@ -0,0 +1,337 @@
+const synchronizeEvent = "synchronize",
+  openedEvent = "opened",
+  completedStatus = "completed",
+  resultSize = 100
+
+class diffHelper {
+  constructor(input) {
+    this.owner = input.context.repo.owner
+    this.repo = input.context.repo.repo
+    this.github = input.github
+    this.pullRequestNumber = input.context.payload.pull_request.number
+    this.pullRequestEvent = input.event
+    this.testName = input.testName
+    this.fileNameFilter = !input.fileNameFilter ? () => true : input.fileNameFilter
+    this.fileLineFilter = !input.fileLineFilter ? () => true : input.fileLineFilter
+  }
+
+  /*
+    Checks whether the test defined by this.testName has been executed on the given commit
+    @param {string} commit - commit SHA to check for test execution
+    @returns {boolean} - returns true if the test has been executed on the commit, otherwise false
+  */
+  async #isTestExecutedOnCommit(commit) {
+    const response = await this.github.rest.checks.listForRef({
+      owner: this.owner,
+      repo: this.repo,
+      ref: commit,
+    })
+
+    return response.data.check_runs.some(
+      ({ status, name }) => status === completedStatus && name === this.testName
+    )
+  }
+
+  /*
+    Retrieves the line numbers of added or updated lines in the provided files
+    @param {Array} files - array of files containing their filename and patch
+    @returns {Object} - object mapping filenames to arrays of line numbers indicating the added or updated lines
+  */
+  async #getDiffForFiles(files = []) {
+    let diff = {}
+    for (const { filename, patch } of files) {
+      if (this.fileNameFilter(filename)) {
+        const lines = patch.split("\n")
+        if (lines.length === 1) {
+          continue
+        }
+
+        let lineNumber
+        for (const line of lines) {
+          // Check if line is diff header
+          //  example:
+          //    @@ -1,3 +1,3 @@
+          //    1    var a
+          //    2
+          //    3   - //test
+          //    3   +var b
+          // Here @@ -1,3 +1,3 @@ is diff header
+          if (line.match(/@@\s.*?@@/) != null) {
+            lineNumber = parseInt(line.match(/\+(\d+)/)[0])
+            continue
+          }
+
+          // "-" prefix indicates line was deleted. So do not consider deleted line
+          if (line.startsWith("-")) {
+            continue
+          }
+
+          // "+"" prefix indicates line was added or updated. Include line number in diff details
+          if (line.startsWith("+") && this.fileLineFilter(line)) {
+            diff[filename] = diff[filename] || []
+            diff[filename].push(lineNumber)
+          }
+          lineNumber++
+        }
+      }
+    }
+    return diff
+  }
+
+  /*
+    Retrieves a list of commits that have not been checked by the test defined by this.testName
+    @returns {Array} - array of commit SHAs that have not been checked by the test
+  */
+  async #getNonScannedCommits() {
+    const { data } = await this.github.rest.pulls.listCommits({
+      owner: this.owner,
+      repo: this.repo,
+      pull_number: this.pullRequestNumber,
+      per_page: resultSize,
+    })
+    let nonScannedCommits = []
+
+    // API returns commits in ascending order. Loop in reverse to quickly retrieve unchecked commits
+    for (let i = data.length - 1; i >= 0; i--) {
+      const { sha, parents } = data[i]
+
+      // Commit can be merged master commit. Such commit have multiple parents
+      // Do not consider such commit for building file diff
+      if (parents.length > 1) {
+        continue
+      }
+
+      const isTestExecuted = await this.#isTestExecutedOnCommit(sha)
+      if (isTestExecuted) {
+        // Remaining commits have been tested in previous scans. Therefore, do not need to be considered again
+        break
+      } else {
+        nonScannedCommits.push(sha)
+      }
+    }
+
+    // Reverse to return commits in ascending order. This is needed to build diff for commits in chronological order
+    return nonScannedCommits.reverse()
+  }
+
+  /*
+    Filters the commit diff to include only the files that are part of the PR diff
+    @param {Array} commitDiff - array of line numbers representing lines added or updated in the commit
+    @param {Array} prDiff - array of line numbers representing lines added or updated in the pull request
+    @returns {Array} - filtered commit diff, including only the files that are part of the PR diff
+  */
+  async #filterCommitDiff(commitDiff = [], prDiff = []) {
+    return commitDiff.filter((file) => prDiff.includes(file))
+  }
+
+  /*
+    Builds the diff for the pull request, including both the changes in the pull request and the changes in non-scanned commits
+    @returns {string} - json string representation of the pull request diff and the diff for non-scanned commits
+  */
+  async buildDiff() {
+    const { data } = await this.github.rest.pulls.listFiles({
+      owner: this.owner,
+      repo: this.repo,
+      pull_number: this.pullRequestNumber,
+      per_page: resultSize,
+    })
+
+    const pullRequestDiff = await this.#getDiffForFiles(data)
+
+    const nonScannedCommitsDiff =
+      Object.keys(pullRequestDiff).length != 0 && this.pullRequestEvent === synchronizeEvent // The "synchronize" event implies that new commit are pushed after the pull request was opened
+        ? await this.getNonScannedCommitDiff(pullRequestDiff)
+        : {}
+
+    const prDiffFiles = Object.keys(pullRequestDiff)
+    const pullRequest = {
+      hasChanges: prDiffFiles.length > 0,
+      files: prDiffFiles.join(" "),
+      diff: pullRequestDiff,
+    }
+    const uncheckedCommits = { diff: nonScannedCommitsDiff }
+    return JSON.stringify({ pullRequest, uncheckedCommits })
+  }
+
+  /*
+    Retrieves the diff for non-scanned commits by comparing their changes with the pull request diff
+    @param {Object} pullRequestDiff - The diff of files in the pull request
+    @returns {Object} - The diff of files in the non-scanned commits that are part of the pull request diff
+   */
+  async getNonScannedCommitDiff(pullRequestDiff) {
+    let nonScannedCommitsDiff = {}
+    // Retrieves list of commits that have not been scanned by the PR check
+    const nonScannedCommits = await this.#getNonScannedCommits()
+    for (const commit of nonScannedCommits) {
+      const { data } = await this.github.rest.repos.getCommit({
+        owner: this.owner,
+        repo: this.repo,
+        ref: commit,
+      })
+
+      const commitDiff = await this.#getDiffForFiles(data.files)
+      const files = Object.keys(commitDiff)
+      for (const file of files) {
+        // Consider scenario where the changes made to a file in the initial commit are completely undone by subsequent commits
+        // In such cases, the modifications from the initial commit should not be taken into account
+        // If the changes were entirely removed, there should be no entry for the file in the pullRequestStats
+        const filePRDiff = pullRequestDiff[file]
+        if (!filePRDiff) {
+          continue
+        }
+
+        // Consider scenario where changes made in the commit were partially removed or modified by subsequent commits
+        // In such cases, include only those commit changes that are part of the pullRequestStats object
+        // This ensures that only the changes that are reflected in the pull request are considered
+        const changes = await this.#filterCommitDiff(commitDiff[file], filePRDiff)
+
+        if (changes.length !== 0) {
+          // Check if nonScannedCommitsDiff[file] exists, if not assign an empty array to it
+          nonScannedCommitsDiff[file] = nonScannedCommitsDiff[file] || []
+          // Combine the existing nonScannedCommitsDiff[file] array with the commit changes
+          // Remove any duplicate elements using the Set data structure
+          nonScannedCommitsDiff[file] = [
+            ...new Set([...nonScannedCommitsDiff[file], ...changes]),
+          ]
+        }
+      }
+    }
+    return nonScannedCommitsDiff
+  }
+}
+
+class semgrepHelper {
+  constructor(input) {
+    this.owner = input.context.repo.owner
+    this.repo = input.context.repo.repo
+    this.github = input.github
+
+    this.pullRequestNumber = input.context.payload.pull_request.number
+    this.pullRequestEvent = input.event
+
+    this.pullRequestDiff = input.diff.pullRequest.diff
+    this.newCommitsDiff = input.diff.uncheckedCommits.diff
+
+    this.semgrepErrors = []
+    this.semgrepWarnings = []
+    input.semgrepResult.forEach((res) => {
+      res.severity === "High" ? this.semgrepErrors.push(res) : this.semgrepWarnings.push(res)
+    })
+
+    this.headSha = input.headSha
+  }
+
+  /*
+    Retrieves the matching line number from the provided diff for a given file and range of lines
+    @param {Object} range - object containing the file, start line, and end line to find a match
+    @param {Object} diff - object containing file changes and corresponding line numbers
+    @returns {number|null} - line number that matches the range within the diff, or null if no match is found
+  */
+  async #getMatchingLineFromDiff({ file, start, end }, diff) {
+    const fileDiff = diff[file]
+    if (!fileDiff) {
+      return null
+    }
+    if (fileDiff.includes(start)) {
+      return start
+    }
+    if (fileDiff.includes(end)) {
+      return end
+    }
+    return null
+  }
+
+  /*
+    Splits the semgrep results into different categories based on the scan
+    @param {Array} semgrepResults - array of results reported by semgrep
+    @returns {Object} - object containing the categorized semgrep results i.e results reported in previous scans and new results found in the current scan
+  */
+  async #splitSemgrepResultsByScan(semgrepResults = []) {
+    const result = {
+      nonDiff: [], // Errors or warnings found in files updated in pull request, but not part of sections that were modified in the pull request
+      previous: [], // Errors or warnings found in previous semgrep scans
+      current: [], // Errors or warnings found in current semgrep scan
+    }
+
+    for (const se of semgrepResults) {
+      const prDiffLine = await this.#getMatchingLineFromDiff(se, this.pullRequestDiff)
+      if (!prDiffLine) {
+        result.nonDiff.push({ ...se })
+        continue
+      }
+
+      switch (this.pullRequestEvent) {
+        case openedEvent:
+          // "Opened" event implies that this is the first check
+          // Therefore, the error should be appended to the result.current
+          result.current.push({ ...se, line: prDiffLine })
+        case synchronizeEvent:
+          const commitDiffLine = await this.#getMatchingLineFromDiff(se, this.newCommitsDiff)
+          // Check if error or warning is part of current commit diff
+          // If not then error or warning was reported in previous scans
+          commitDiffLine != null
+            ? result.current.push({ ...se, line: commitDiffLine })
+            : result.previous.push({
+                ...se,
+                line: prDiffLine,
+              })
+      }
+    }
+    return result
+  }
+
+  /*
+    Adds review comments based on the semgrep results to the current pull request
+    @returns {Object} - object containing the count of unaddressed comments from the previous scan and the count of new comments from the current scan
+  */
+  async addReviewComments() {
+    let result = {
+      previousScan: { unAddressedComments: 0 },
+      currentScan: { newComments: 0 },
+    }
+
+    if (this.semgrepErrors.length == 0 && this.semgrepWarnings.length == 0) {
+      return result
+    }
+
+    const errors = await this.#splitSemgrepResultsByScan(this.semgrepErrors)
+    if (errors.previous.length == 0 && errors.current.length == 0) {
+      console.log("Semgrep did not find any errors in the current pull request changes")
+    } else {
+      for (const { message, file, line } of errors.current) {
+        await this.github.rest.pulls.createReviewComment({
+          owner: this.owner,
+          repo: this.repo,
+          pull_number: this.pullRequestNumber,
+          commit_id: this.headSha,
+          body: message,
+          path: file,
+          line: line,
+        })
+      }
+      result.currentScan.newComments = errors.current.length
+      if (this.pullRequestEvent == synchronizeEvent) {
+        result.previousScan.unAddressedComments = errors.previous.length
+      }
+    }
+
+    const warnings = await this.#splitSemgrepResultsByScan(this.semgrepWarnings)
+    for (const { message, file, line } of warnings.current) {
+      await this.github.rest.pulls.createReviewComment({
+        owner: this.owner,
+        repo: this.repo,
+        pull_number: this.pullRequestNumber,
+        commit_id: this.headSha,
+        body: "Consider this as a suggestion. " + message,
+        path: file,
+        line: line,
+      })
+    }
+    return result
+  }
+}
+
+module.exports = {
+  diffHelper: (input) => new diffHelper(input),
+  semgrepHelper: (input) => new semgrepHelper(input),
+}
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
new file mode 100644
index 00000000..87c5573e
--- /dev/null
+++ b/.github/workflows/semgrep.yml
@@ -0,0 +1,74 @@
+name: Adapter semgrep checks
+on:
+  pull_request:
+    paths: ["adapters/*/*.go"]
+permissions: write-all
+jobs:
+  semgrep-check:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout repo
+        uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+
+      - name: Calculate diff
+        id: calculate_diff
+        uses: actions/github-script@v6
+        with:
+          result-encoding: string
+          script: |
+            const utils = require('./.github/workflows/helpers/pull-request-utils.js')
+            // consider only non-test Go files that are part of the adapter code
+            function fileNameFilter(filename) {
+              return filename.startsWith("adapters/") && filename.split("/").length > 2 && filename.endsWith(".go") && !filename.endsWith("_test.go")
+            }
+            const helper = utils.diffHelper({github, context, fileNameFilter, event: "${{github.event.action}}", testName: "${{github.job}}"})
+            return await helper.buildDiff()
+
+      - name: Should run semgrep
+        id: should_run_semgrep
+        run: |
+          hasChanges=$(echo '${{ steps.calculate_diff.outputs.result }}' | jq .pullRequest.hasChanges)
+          echo "hasChanges=${hasChanges}" >> $GITHUB_OUTPUT
+
+      - name: Install semgrep
+        if: contains(steps.should_run_semgrep.outputs.hasChanges, 'true')
+        run: |
+          pip3 install semgrep==1.22.0
+          semgrep --version
+
+      - name: Run semgrep tests
+        id: run_semgrep_tests
+        if: contains(steps.should_run_semgrep.outputs.hasChanges, 'true')
+        run: |
+          unqouted_string=$(echo '${{ steps.calculate_diff.outputs.result }}' | jq .pullRequest.files | tr -d '"')
+          outputs=$(semgrep --gitlab-sast --config=.semgrep/adapter $unqouted_string  | jq '[.vulnerabilities[] | {"file": .location.file, "severity": .severity, "start": .location.start_line, "end": .location.end_line, "message": (.message | gsub("\\n"; "\n"))}]' | jq -c | jq -R)
+          echo "semgrep_result=${outputs}" >> "$GITHUB_OUTPUT"
+
+      - name: Add pull request comment
+        id: add_pull_request_comment
+        if: contains(steps.should_run_semgrep.outputs.hasChanges, 'true')
+        uses: actions/github-script@v6.4.1
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          result-encoding: string
+          script: |
+            const utils = require('./.github/workflows/helpers/pull-request-utils.js')
+            const helper = utils.semgrepHelper({
+                github, context, event: "${{github.event.action}}", 
+                semgrepResult: JSON.parse(${{ steps.run_semgrep_tests.outputs.semgrep_result }}), 
+                diff: ${{ steps.calculate_diff.outputs.result }}, headSha: "${{github.event.pull_request.head.sha}}"
+            })
+            const { previousScan, currentScan } = await helper.addReviewComments()
+            return previousScan.unAddressedComments + currentScan.newComments
+
+      - name: Adapter semgrep checks result
+        if: contains(steps.should_run_semgrep.outputs.hasChanges, 'true')
+        run: |
+          if [ "${{steps.add_pull_request_comment.outputs.result}}" -ne "0" ]; then
+              echo 'Semgrep has found "${{steps.add_pull_request_comment.outputs.result}}" errors'
+              exit 1
+          else
+              echo 'Semgrep did not find any errors in the pull request changes'
+          fi
diff --git a/.semgrep/adapter/builder-struct-name.go b/.semgrep/adapter/builder-struct-name.go
new file mode 100644
index 00000000..37a938ca
--- /dev/null
+++ b/.semgrep/adapter/builder-struct-name.go
@@ -0,0 +1,124 @@
+/*
+	builder-struct-name tests
+	https://semgrep.dev/docs/writing-rules/testing-rules
+	"ruleid" prefix in comment indicates patterns that should be flagged by semgrep
+	"ok" prefix in comment indidcates  patterns that should not be flagged by the semgrep
+*/
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	foo1 := foo{}
+	// ruleid: builder-struct-name-check
+	return &fooadapter{foo: foo1}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	// ruleid: builder-struct-name-check
+	return &adapterbar{}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	// ruleid: builder-struct-name-check
+	return &fooadapterbar{}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	// ruleid: builder-struct-name-check
+	return &FooAdapter{}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	// ruleid: builder-struct-name-check
+	return &AdapterBar{}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	// ruleid: builder-struct-name-check
+	return &AdapterBar{}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	// ruleid: builder-struct-name-check
+	return &FooAdapterBar{}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	foo2 := foo{}
+	//ruleid: builder-struct-name-check
+	adpt1 := Adapter{foo: foo2}
+	return &adpt1, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	//ruleid: builder-struct-name-check
+	builder := &Adapter{foo{}}
+	return builder, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	foo3 := foo{}
+	if foo3.bar == "" {
+		foo3.bar = "bar"
+	}
+	//ruleid: builder-struct-name-check
+	adpt2 := Adapter{}
+	adpt2.foo = foo3
+	return &adpt2, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	//ruleid: builder-struct-name-check
+	return &foo{}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	//ruleid: builder-struct-name-check
+	var obj Adapter
+	obj.Foo = "foo"
+	if obj.Bar == "" {
+		obj.Bar = "bar"
+	}
+	return &obj, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	//ruleid: builder-struct-name-check
+	var obj *FooAdapterBar
+	obj.Foo = "foo"
+	if obj.Bar == "" {
+		obj.Bar = "bar"
+	}
+	return obj, nil
+}
+
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+	// ok: builder-struct-name-check
+	return &adapter{endpoint: "www.foo.com"}, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	builder := &adapter{}
+	builder.endpoint = "www.foo.com"
+	// ok: builder-struct-name-check
+	return builder, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	builder := adapter{}
+	builder.endpoint = "www.foo.com"
+	// ok: builder-struct-name-check
+	return &builder, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	var builder adapter
+	builder.endpoint = "www.foo.com"
+	// ok: builder-struct-name-check
+	return &builder, nil
+}
+
+func Builder(_ openrtb_ext.BidderName, config config.Adapter, _ config.Server) (adapters.Bidder, error) {
+	var builder *adapter
+	builder.endpoint = "www.foo.com"
+	// ok: builder-struct-name-check
+	return builder, nil
+}
\ No newline at end of file
diff --git a/.semgrep/adapter/builder-struct-name.yml b/.semgrep/adapter/builder-struct-name.yml
new file mode 100644
index 00000000..bc876ae1
--- /dev/null
+++ b/.semgrep/adapter/builder-struct-name.yml
@@ -0,0 +1,58 @@
+rules:
+  - id: builder-struct-name-check
+    languages:
+      - go
+    message: | 
+      You can call this simply "adapter", the `$BUILDER` identification is already supplied by the package name. As you have it, referencing your adapter from outside the package would be `$BUILDER.$BUILDER` which looks a little redundant. See example below:
+      
+      ```
+        package foo
+
+        type adapter struct {
+          endpoint string
+        }
+        
+        func  Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+          return &adapter{endpoint: "https://www.foo.com"}, nil
+        }
+      ```
+    severity: ERROR
+    patterns:
+      - pattern-either:
+          - pattern-inside: >
+              func Builder($BIDDER_NAME openrtb_ext.BidderName, $CONFIG config.Adapter, $SERVER config.Server) (adapters.Bidder, error) {
+                ...
+                $BUILDER_OBJ := &$BUILDER{...}
+                ...
+                return $BUILDER_OBJ, nil
+              }
+          - pattern-inside: >
+              func Builder($BIDDER_NAME openrtb_ext.BidderName, $CONFIG config.Adapter, $SERVER config.Server) (adapters.Bidder, error) {
+                ...
+                $BUILDER_OBJ := $BUILDER{...}
+                ...
+                return &$BUILDER_OBJ, nil
+              }
+          - pattern-inside: >
+              func Builder($BIDDER_NAME openrtb_ext.BidderName, $CONFIG config.Adapter, $SERVER config.Server) (adapters.Bidder, error) {
+                ...
+                return &$BUILDER{...}, ...
+              }
+          - pattern-inside: >
+              func Builder($BIDDER_NAME openrtb_ext.BidderName, $CONFIG config.Adapter, $SERVER config.Server) (adapters.Bidder, error) {
+                ...
+                var $BUILDER_OBJ $BUILDER
+                ...
+                return &$BUILDER_OBJ, ...
+              }            
+          - pattern-inside: >
+              func Builder($BIDDER_NAME openrtb_ext.BidderName, $CONFIG config.Adapter, $SERVER config.Server) (adapters.Bidder, error) {
+                ...
+                var $BUILDER_OBJ *$BUILDER
+                ...
+                return $BUILDER_OBJ, ...
+              }        
+      - focus-metavariable: $BUILDER
+      - metavariable-regex:
+          metavariable: $BUILDER
+          regex: (?!adapter$)
diff --git a/.semgrep/adapter/package-import.go b/.semgrep/adapter/package-import.go
new file mode 100644
index 00000000..a34ce878
--- /dev/null
+++ b/.semgrep/adapter/package-import.go
@@ -0,0 +1,47 @@
+import (
+	// ok: package-import-check
+	"fmt"
+	// ok: package-import-check
+	"os"
+	// ruleid: package-import-check
+	"github.com/mitchellh/copystructure"
+	// ruleid: package-import-check
+	"github.com/golang/glog"
+)
+
+import (
+	// ok: package-import-check
+	"fmt"
+	// ruleid: package-import-check
+	cs "github.com/mitchellh/copystructure"
+	// ok: package-import-check
+	"os"
+	// ruleid: package-import-check
+	log "github.com/golang/glog"
+)
+
+import (
+	// ok: package-import-check
+	"fmt"
+	// ruleid: package-import-check
+	cs "github.com/mitchellh/copystructure/subpackage"
+	// ok: package-import-check
+	"os"
+	// ruleid: package-import-check
+	log "github.com/golang/glog/subpackage"
+)
+
+// ruleid: package-import-check
+import "github.com/golang/glog"
+
+// ruleid: package-import-check
+import "github.com/mitchellh/copystructure"
+
+// ruleid: package-import-check
+import log "github.com/golang/glog"
+
+// ruleid: package-import-check
+import copy "github.com/mitchellh/copystructure"
+
+// ok: package-import-check
+import "fmt"  
diff --git a/.semgrep/adapter/package-import.yml b/.semgrep/adapter/package-import.yml
new file mode 100644
index 00000000..20de2d10
--- /dev/null
+++ b/.semgrep/adapter/package-import.yml
@@ -0,0 +1,13 @@
+rules:
+  - id: package-import-check
+    message: Usage of "$PKG" package is prohibited in adapter code
+    languages:
+      - go
+    severity: ERROR
+    pattern-either:
+      - patterns:
+          - pattern: import "$PKG"
+          - focus-metavariable: $PKG
+          - metavariable-regex:
+              metavariable: $PKG
+              regex: (^github\.com/mitchellh/copystructure(/.*)?$|^github\.com/golang/glog(/.*)?$)
\ No newline at end of file
diff --git a/.semgrep/adapter/type-bid-assignment.go b/.semgrep/adapter/type-bid-assignment.go
new file mode 100644
index 00000000..fb29a491
--- /dev/null
+++ b/.semgrep/adapter/type-bid-assignment.go
@@ -0,0 +1,181 @@
+/*
+	type-bid-assignment tests
+	https://semgrep.dev/docs/writing-rules/testing-rules
+	"ruleid" prefix in comment indicates patterns that should be flagged by semgrep
+	"ok" prefix in comment indidcates  patterns that should not be flagged by the semgrep
+*/
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var bidResp openrtb2.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	for _, seatBid := range bidResp.SeatBid {
+		for _, sb := range seatBid.Bid {
+			bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp)
+			if err != nil {
+				errs = append(errs, err)
+			} else {
+				b := &adapters.TypedBid{
+					// ruleid: type-bid-assignment-check
+					Bid:     &sb,
+					BidType: bidType,
+				}
+				bidResponse.Bids = append(bidResponse.Bids, b)
+			}
+		}
+	}
+}
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var bidResp openrtb2.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	for _, seatBid := range bidResp.SeatBid {
+		for _, sb := range seatBid.Bid {
+			sbcopy := sb
+			bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp)
+			if err != nil {
+				errs = append(errs, err)
+			} else {
+				b := &adapters.TypedBid{
+					// ok: type-bid-assignment-check
+					Bid:     &sbcopy,
+					BidType: bidType,
+				}
+				bidResponse.Bids = append(bidResponse.Bids, b)
+			}
+		}
+	}
+}
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var bidResp openrtb2.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	for _, seatBid := range bidResp.SeatBid {
+		for _, sb := range seatBid.Bid {
+			bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp)
+			if err != nil {
+				return nil, err
+			}
+			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+				// ruleid: type-bid-assignment-check
+				Bid:     &sb,
+				BidType: bidType,
+			})
+
+		}
+	}
+}
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var bidResp openrtb2.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	for _, seatBid := range bidResp.SeatBid {
+		for _, sb := range seatBid.Bid {
+			bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp)
+			if err != nil {
+				return nil, err
+			}
+			// ruleid: type-bid-assignment-check
+			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{Bid: &sb, BidType: bidType})
+		}
+	}
+}
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var bidResp openrtb2.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	var errors []error
+	for _, seatBid := range bidResp.SeatBid {
+		for _, bid := range seatBid.Bid {
+			var t adapters.TypedBid
+			// ruleid: type-bid-assignment-check
+			t.Bid = &bid
+			bidResponse.Bids = append(bidResponse.Bids, &t)
+		}
+	}
+	return bidResponse, errors
+}
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var bidResp openrtb2.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	var errors []error
+	for _, seatBid := range bidResp.SeatBid {
+		for _, bid := range seatBid.Bid {
+			var t adapters.TypedBid
+			t = adapters.TypedBid{
+				// ruleid: type-bid-assignment-check
+				Bid: &bid,
+			}
+
+			bidResponse.Bids = append(bidResponse.Bids, &t)
+		}
+	}
+	return bidResponse, errors
+}
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var bidResp openrtb2.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	for _, seatBid := range bidResp.SeatBid {
+		for idx, _ := range seatBid.Bid {
+			bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp)
+			if err != nil {
+				errs = append(errs, err)
+			} else {
+				b := &adapters.TypedBid{
+					// ok: type-bid-assignment-check
+					Bid:     &seatBid.Bid[idx],
+					BidType: bidType,
+				}
+				bidResponse.Bids = append(bidResponse.Bids, b)
+			}
+		}
+	}
+}
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	var bidResp openrtb2.BidResponse
+
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	for _, seatBid := range bidResp.SeatBid {
+		for idx := range seatBid.Bid {
+			bidType, err := getMediaTypeForImp(seatBid.Bid[i], internalRequest.Imp)
+			if err != nil {
+				return nil, err
+			}
+			// ok: type-bid-assignment-check
+			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{Bid: &seatBid.Bid[idx], BidType: bidType})
+		}
+	}
+}
diff --git a/.semgrep/adapter/type-bid-assignment.yml b/.semgrep/adapter/type-bid-assignment.yml
new file mode 100644
index 00000000..95fb6811
--- /dev/null
+++ b/.semgrep/adapter/type-bid-assignment.yml
@@ -0,0 +1,64 @@
+rules:
+  - id: type-bid-assignment-check
+    languages:
+      - go
+    message: >      
+      Found incorrect assignment made to $KEY. $BID variable receives a new value in each iteration of range loop. Assigning the address of $BID `(&$BID)` to $KEY will result in a pointer that always points to the same memory address with the value of the last iteration. 
+      This can lead to unexpected behavior or incorrect results. Refer https://go.dev/play/p/9ZS1f-5h4qS
+
+      Consider using an index variable in the seatBids.Bid loop as shown below
+
+      ```
+        for _, seatBid := range response.SeatBid {
+          for i := range seatBids.Bid {
+            ...
+            responseBid := &adapters.TypedBid{
+              Bid: &seatBids.Bid[i],
+              ...
+            }
+            ...
+            ...
+          }
+        }
+      ```
+    severity: ERROR
+    patterns:
+      - pattern-either:
+          - pattern: >
+              for _, $BID := range ... {
+                ...
+                ... := &adapters.TypedBid{
+                  $KEY: &$BID,
+                  ...
+                }
+                ...
+              }
+          - pattern: >
+              for _, $BID := range ... {
+                ...
+                ... = adapters.TypedBid{
+                  $KEY: &$BID,
+                  ...
+                }
+                ...
+              } 
+          - pattern: >
+              for _, $BID := range ... {
+                ...
+                ... = append(...,  &adapters.TypedBid{
+                  $KEY: &$BID,
+                  ...
+                })
+                ...
+              }
+          - pattern: >
+              for _, $BID := range ... {
+                var $TYPEBID_OBJ adapters.TypedBid
+                ...
+                $TYPEBID_OBJ.$KEY = &$BID
+                ...
+              }                              
+      - focus-metavariable: $KEY
+      - metavariable-regex:
+          metavariable: $KEY
+          regex: Bid
diff --git a/adapters/ix/ix.go b/adapters/ix/ix.go
index 919f58da..796fefbb 100644
--- a/adapters/ix/ix.go
+++ b/adapters/ix/ix.go
@@ -7,7 +7,6 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/golang/glog"
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/errortypes"
@@ -123,7 +122,6 @@ func setSitePublisherId(requestCopy *openrtb2.BidRequest, uniqueSiteIDs map[stri
 			// Sorting siteIDs for predictable output as Go maps don't guarantee order
 			sort.Strings(siteIDs)
 			multipleSiteIDs := strings.Join(siteIDs, ", ")
-			glog.Warningf("Multiple SiteIDs found. %s", multipleSiteIDs)
 			ixDiag.MultipleSiteIds = multipleSiteIDs
 		}
 		requestCopy.Site = &site
diff --git a/config/account.go b/config/account.go
index 8beff9b6..02040211 100644
--- a/config/account.go
+++ b/config/account.go
@@ -40,7 +40,7 @@ type Account struct {
 	Validations             Validations                                 `mapstructure:"validations" json:"validations"`
 	DefaultBidLimit         int                                         `mapstructure:"default_bid_limit" json:"default_bid_limit"`
 	BidAdjustments          *openrtb_ext.ExtRequestPrebidBidAdjustments `mapstructure:"bidadjustments" json:"bidadjustments"`
-	Privacy                 AccountPrivacy                              `mapstructure:"privacy" json:"privacy"`
+	Privacy                 *AccountPrivacy                             `mapstructure:"privacy" json:"privacy"`
 }
 
 // CookieSync represents the account-level defaults for the cookie sync endpoint.
diff --git a/config/activity.go b/config/activity.go
index 987cbe84..5bddc7c6 100644
--- a/config/activity.go
+++ b/config/activity.go
@@ -8,16 +8,17 @@ type AllowActivities struct {
 	TransmitUserFPD          Activity `mapstructure:"transmitUfpd" json:"transmitUfpd"`
 	TransmitPreciseGeo       Activity `mapstructure:"transmitPreciseGeo" json:"transmitPreciseGeo"`
 	TransmitUniqueRequestIds Activity `mapstructure:"transmitUniqueRequestIds" json:"transmitUniqueRequestIds"`
+	TransmitTids             Activity `mapstructure:"transmitTid" json:"transmitTid"`
 }
 
 type Activity struct {
 	Default *bool          `mapstructure:"default" json:"default"`
 	Rules   []ActivityRule `mapstructure:"rules" json:"rules"`
-	Allow   bool           `mapstructure:"allow" json:"allow"`
 }
 
 type ActivityRule struct {
 	Condition ActivityCondition `mapstructure:"condition" json:"condition"`
+	Allow     bool              `mapstructure:"allow" json:"allow"`
 }
 
 type ActivityCondition struct {
diff --git a/config/config.go b/config/config.go
index ee18c746..543d1afd 100644
--- a/config/config.go
+++ b/config/config.go
@@ -155,6 +155,10 @@ func (cfg *Configuration) validate(v *viper.Viper) []error {
 		cfg.TmaxAdjustments.Enabled = false
 	}
 
+	if cfg.AccountDefaults.Privacy != nil {
+		glog.Warning("account_defaults.Privacy has no effect as the feature is under development.")
+	}
+
 	errs = cfg.Experiment.validate(errs)
 	errs = cfg.BidderInfos.validate(errs)
 	return errs
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 73f4842a..8d328b7c 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/prebid/prebid-server/privacy"
 	"net/http"
 	"net/url"
 	"strings"
@@ -222,6 +223,13 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 
 	tcf2Config := gdpr.NewTCF2Config(deps.cfg.GDPR.TCF2, account.GDPR)
 
+	activities, activitiesErr := privacy.NewActivityControl(account.Privacy)
+	if activitiesErr != nil {
+		errL = append(errL, activitiesErr)
+		writeError(errL, w, &labels)
+		return
+	}
+
 	secGPC := r.Header.Get("Sec-GPC")
 
 	auctionRequest := &exchange.AuctionRequest{
@@ -239,6 +247,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 		HookExecutor:               hookExecutor,
 		QueryParams:                r.URL.Query(),
 		TCF2Config:                 tcf2Config,
+		Activities:                 activities,
 	}
 
 	auctionResponse, err := deps.ex.HoldAuction(ctx, auctionRequest, nil)
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 216bff6e..3f01967a 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/prebid/prebid-server/privacy"
 	"io"
 	"io/ioutil"
 	"net/http"
@@ -191,6 +192,15 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 
 	tcf2Config := gdpr.NewTCF2Config(deps.cfg.GDPR.TCF2, account.GDPR)
 
+	activities, activitiesErr := privacy.NewActivityControl(account.Privacy)
+	if activitiesErr != nil {
+		errL = append(errL, activitiesErr)
+		if errortypes.ContainsFatalError(errL) {
+			writeError(errL, w, &labels)
+			return
+		}
+	}
+
 	ctx := context.Background()
 
 	timeout := deps.cfg.AuctionTimeouts.LimitAuctionTimeout(time.Duration(req.TMax) * time.Millisecond)
@@ -236,6 +246,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 		PubID:                      labels.PubID,
 		HookExecutor:               hookExecutor,
 		TCF2Config:                 tcf2Config,
+		Activities:                 activities,
 	}
 	auctionResponse, err := deps.ex.HoldAuction(ctx, auctionRequest, nil)
 	ao.RequestWrapper = req
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 24ca193f..07dce45a 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/prebid/prebid-server/privacy"
 	"math/rand"
 	"net/url"
 	"runtime/debug"
@@ -195,6 +196,7 @@ type AuctionRequest struct {
 	GlobalPrivacyControlHeader string
 	ImpExtInfoMap              map[string]ImpExtInfo
 	TCF2Config                 gdpr.TCF2ConfigReader
+	Activities                 privacy.ActivityControl
 
 	// LegacyLabels is included here for temporary compatibility with cleanOpenRTBRequests
 	// in HoldAuction until we get to factoring it away. Do not use for anything new.
diff --git a/exchange/utils.go b/exchange/utils.go
index d43a4182..5b29fd62 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -150,6 +150,15 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
 	for _, bidderRequest := range allBidderRequests {
 		bidRequestAllowed := true
 
+		// fetchBids activity
+		fetchBidsActivityAllowed := auctionReq.Activities.Allow(privacy.ActivityFetchBids,
+			privacy.ScopedName{Scope: privacy.ScopeTypeBidder, Name: bidderRequest.BidderName.String()})
+		if fetchBidsActivityAllowed == privacy.ActivityDeny {
+			// skip the call to a bidder if fetchBids activity is not allowed
+			// do not add this bidder to allowedBidderRequests
+			continue
+		}
+
 		// CCPA
 		privacyEnforcement.CCPA = ccpaEnforcer.ShouldEnforce(bidderRequest.BidderName.String())
 
diff --git a/go.mod b/go.mod
index 3833ef3d..e7a6f7e3 100644
--- a/go.mod
+++ b/go.mod
@@ -33,14 +33,14 @@ require (
 	github.com/yudai/gojsondiff v1.0.0
 	golang.org/x/net v0.7.0
 	golang.org/x/text v0.7.0
-	google.golang.org/grpc v1.46.2
+	google.golang.org/grpc v1.53.0
 	gopkg.in/evanphx/json-patch.v4 v4.12.0
 	gopkg.in/yaml.v3 v3.0.1
 )
 
 require (
 	github.com/beorn7/perks v1.0.1 // indirect
-	github.com/cespare/xxhash/v2 v2.1.2 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
@@ -68,8 +68,8 @@ require (
 	github.com/yudai/pp v2.0.1+incompatible // indirect
 	golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
 	golang.org/x/sys v0.5.0 // indirect
-	google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect
-	google.golang.org/protobuf v1.28.0 // indirect
+	google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
+	google.golang.org/protobuf v1.28.1 // indirect
 	gopkg.in/ini.v1 v1.66.4 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 )
diff --git a/go.sum b/go.sum
index e1226ab1..1f76be7a 100644
--- a/go.sum
+++ b/go.sum
@@ -86,8 +86,9 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
 github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
 github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chasex/glog v0.0.0-20160217080310-c62392af379c h1:eXqCBUHfmjbeDqcuvzjsd+bM6A+bnwo5N9FVbV6m5/s=
 github.com/chasex/glog v0.0.0-20160217080310-c62392af379c/go.mod h1:omJZNg0Qu76bxJd+ExohVo8uXzNcGOk2bv7vel460xk=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -125,7 +126,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
 github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
 github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
-github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@@ -210,7 +210,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
-github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -392,8 +392,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
-github.com/prebid/go-gdpr v1.11.0 h1:QbMjscuw3Ul0mDVWeMy5tP0Kii6lmTSSVhV6fm8rY9s=
-github.com/prebid/go-gdpr v1.11.0/go.mod h1:mPZAdkRxn+iuSjaUuJAi9+0SppBOdM1PCzv/55UH3pY=
 github.com/prebid/go-gdpr v1.12.0 h1:OrjQ7Uc+lCRYaOirQ48jjG/PBMvZsKNAaRTgzxN6iZ0=
 github.com/prebid/go-gdpr v1.12.0/go.mod h1:mPZAdkRxn+iuSjaUuJAi9+0SppBOdM1PCzv/55UH3pY=
 github.com/prebid/go-gpp v0.1.1 h1:uTMJ+eHmKWL9WvDuxFT4LDoOeJW1yOsfWITqi49ZuY0=
@@ -910,8 +908,8 @@ google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ6
 google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd h1:e0TwkXOdbnH/1x5rc5MZ/VYyiZ4v+RdVfrGMqEwT68I=
-google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
+google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=
+google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
 google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -942,9 +940,8 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K
 google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
 google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
 google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
-google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
-google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ=
-google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc=
+google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -959,8 +956,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
-google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/privacy/activity.go b/privacy/activity.go
index 7cdef175..6ca48ae0 100644
--- a/privacy/activity.go
+++ b/privacy/activity.go
@@ -11,6 +11,7 @@ const (
 	ActivityTransmitUserFPD
 	ActivityTransmitPreciseGeo
 	ActivityTransmitUniqueRequestIds
+	ActivityTransmitTids
 )
 
 func (a Activity) String() string {
@@ -29,6 +30,8 @@ func (a Activity) String() string {
 		return "transmitPreciseGeo"
 	case ActivityTransmitUniqueRequestIds:
 		return "transmitUniqueRequestIds"
+	case ActivityTransmitTids:
+		return "transmitTid"
 	}
 
 	return ""
diff --git a/privacy/enforcer.go b/privacy/enforcer.go
index 0d5ecad5..d63cd8de 100644
--- a/privacy/enforcer.go
+++ b/privacy/enforcer.go
@@ -1,43 +1,236 @@
 package privacy
 
-// PolicyEnforcer determines if personally identifiable information (PII) should be removed or anonymized per the policy.
-type PolicyEnforcer interface {
-	// CanEnforce returns true when policy information is specifically provided by the publisher.
-	CanEnforce() bool
+import (
+	"fmt"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
+	"strings"
+)
 
-	// ShouldEnforce returns true when the OpenRTB request should have personally identifiable
-	// information (PII) removed or anonymized per the policy.
-	ShouldEnforce(bidder string) bool
+type ActivityResult int
+
+const (
+	ActivityAbstain ActivityResult = iota
+	ActivityAllow
+	ActivityDeny
+)
+
+const (
+	ScopeTypeBidder    = "bidder"
+	ScopeTypeAnalytics = "analytics"
+	ScopeTypeRTD       = "rtd" // real time data
+	ScopeTypeUserID    = "userid"
+	ScopeTypeGeneral   = "general"
+)
+
+type ActivityControl struct {
+	plans map[Activity]ActivityPlan
 }
 
-// NilPolicyEnforcer implements the PolicyEnforcer interface but will always return false.
-type NilPolicyEnforcer struct{}
+func NewActivityControl(privacyConf *config.AccountPrivacy) (ActivityControl, error) {
+	ac := ActivityControl{}
+	var err error
 
-// CanEnforce is hardcoded to always return false.
-func (NilPolicyEnforcer) CanEnforce() bool {
-	return false
+	if privacyConf == nil {
+		return ac, err
+	} else {
+		//temporarily disable Activities if they are specified at the account level
+		return ac, &errortypes.Warning{Message: "account.Privacy has no effect as the feature is under development."}
+	}
+
+	plans := make(map[Activity]ActivityPlan)
+
+	plans[ActivitySyncUser], err = buildEnforcementPlan(privacyConf.AllowActivities.SyncUser)
+	if err != nil {
+		return ac, err
+	}
+	plans[ActivityFetchBids], err = buildEnforcementPlan(privacyConf.AllowActivities.FetchBids)
+	if err != nil {
+		return ac, err
+	}
+	plans[ActivityEnrichUserFPD], err = buildEnforcementPlan(privacyConf.AllowActivities.EnrichUserFPD)
+	if err != nil {
+		return ac, err
+	}
+	plans[ActivityReportAnalytics], err = buildEnforcementPlan(privacyConf.AllowActivities.ReportAnalytics)
+	if err != nil {
+		return ac, err
+	}
+	plans[ActivityTransmitUserFPD], err = buildEnforcementPlan(privacyConf.AllowActivities.TransmitUserFPD)
+	if err != nil {
+		return ac, err
+	}
+	plans[ActivityTransmitPreciseGeo], err = buildEnforcementPlan(privacyConf.AllowActivities.TransmitPreciseGeo)
+	if err != nil {
+		return ac, err
+	}
+	plans[ActivityTransmitUniqueRequestIds], err = buildEnforcementPlan(privacyConf.AllowActivities.TransmitUniqueRequestIds)
+	if err != nil {
+		return ac, err
+	}
+	plans[ActivityTransmitTids], err = buildEnforcementPlan(privacyConf.AllowActivities.TransmitTids)
+	if err != nil {
+		return ac, err
+	}
+
+	ac.plans = plans
+
+	return ac, nil
+}
+
+func buildEnforcementPlan(activity config.Activity) (ActivityPlan, error) {
+	ef := ActivityPlan{}
+	rules, err := activityRulesToEnforcementRules(activity.Rules)
+	if err != nil {
+		return ef, err
+	}
+	ef.defaultResult = activityDefaultToDefaultResult(activity.Default)
+	ef.rules = rules
+	return ef, nil
 }
 
-// ShouldEnforce is hardcoded to always return false.
-func (NilPolicyEnforcer) ShouldEnforce(bidder string) bool {
-	return false
+func activityRulesToEnforcementRules(rules []config.ActivityRule) ([]ActivityRule, error) {
+	enfRules := make([]ActivityRule, 0)
+	for _, r := range rules {
+		cmpName, err := conditionToRuleComponentName(r.Condition.ComponentName)
+		if err != nil {
+			return nil, err
+		}
+		er := ComponentEnforcementRule{
+			allowed:       r.Allow,
+			componentName: cmpName,
+			componentType: r.Condition.ComponentType,
+		}
+		enfRules = append(enfRules, er)
+	}
+	return enfRules, nil
 }
 
-// EnabledPolicyEnforcer decorates a PolicyEnforcer with an enabled flag.
-type EnabledPolicyEnforcer struct {
-	Enabled        bool
-	PolicyEnforcer PolicyEnforcer
+func conditionToRuleComponentName(conditions []string) ([]ScopedName, error) {
+	sn := make([]ScopedName, 0)
+	for _, condition := range conditions {
+		scope, err := NewScopedName(condition)
+		if err != nil {
+			return sn, err
+		}
+		sn = append(sn, scope)
+	}
+	return sn, nil
 }
 
-// CanEnforce returns true when the PolicyEnforcer can enforce.
-func (p EnabledPolicyEnforcer) CanEnforce() bool {
-	return p.PolicyEnforcer.CanEnforce()
+func activityDefaultToDefaultResult(activityDefault *bool) ActivityResult {
+	if activityDefault == nil {
+		// if default is unspecified, the hardcoded default-default is true.
+		return ActivityAllow
+	} else if *activityDefault {
+		return ActivityAllow
+	}
+	return ActivityDeny
 }
 
-// ShouldEnforce returns true when the enforcer is enabled the PolicyEnforcer allows enforcement.
-func (p EnabledPolicyEnforcer) ShouldEnforce(bidder string) bool {
-	if p.Enabled {
-		return p.PolicyEnforcer.ShouldEnforce(bidder)
+func (e ActivityControl) Allow(activity Activity, target ScopedName) ActivityResult {
+	plan, planDefined := e.plans[activity]
+
+	if !planDefined {
+		return ActivityAbstain
 	}
-	return false
+
+	return plan.Allow(target)
+}
+
+type ActivityPlan struct {
+	defaultResult ActivityResult
+	rules         []ActivityRule
+}
+
+func (p ActivityPlan) Allow(target ScopedName) ActivityResult {
+	for _, rule := range p.rules {
+		result := rule.Allow(target)
+		if result == ActivityDeny || result == ActivityAllow {
+			return result
+		}
+	}
+	return p.defaultResult
+}
+
+type ActivityRule interface {
+	Allow(target ScopedName) ActivityResult
+}
+
+type ComponentEnforcementRule struct {
+	componentName []ScopedName
+	componentType []string
+	// include gppSectionId from 3.5
+	// include geo from 3.5
+	allowed bool
+}
+
+func (r ComponentEnforcementRule) Allow(target ScopedName) ActivityResult {
+	if len(r.componentName) == 0 && len(r.componentType) == 0 {
+		return ActivityAbstain
+	}
+
+	nameClauseExists := len(r.componentName) > 0
+	typeClauseExists := len(r.componentType) > 0
+
+	componentNameFound := false
+	for _, scope := range r.componentName {
+		if strings.EqualFold(scope.Scope, target.Scope) &&
+			(scope.Name == "*" || strings.EqualFold(scope.Name, target.Name)) {
+			componentNameFound = true
+			break
+		}
+	}
+
+	componentTypeFound := false
+	for _, componentType := range r.componentType {
+		if strings.EqualFold(componentType, target.Scope) {
+			componentTypeFound = true
+			break
+		}
+	}
+	// behavior if rule matches: can be either true=allow or false=deny. result is abstain if the rule doesn't match
+	matchFound := (componentNameFound || !nameClauseExists) && (componentTypeFound || !typeClauseExists)
+	if matchFound {
+		if r.allowed {
+			return ActivityAllow
+		} else {
+			return ActivityDeny
+		}
+	}
+	return ActivityAbstain
+}
+
+type ScopedName struct {
+	Scope string
+	Name  string
+}
+
+func NewScopedName(condition string) (ScopedName, error) {
+	if condition == "" {
+		return ScopedName{}, fmt.Errorf("unable to parse empty condition")
+	}
+	var scope, name string
+	split := strings.Split(condition, ".")
+	if len(split) == 2 {
+		s := strings.ToLower(split[0])
+		if s == ScopeTypeBidder || s == ScopeTypeAnalytics || s == ScopeTypeUserID {
+			scope = s
+		} else if strings.Contains(s, ScopeTypeRTD) {
+			scope = ScopeTypeRTD
+		} else {
+			scope = ScopeTypeGeneral
+		}
+		name = split[1]
+	} else if len(split) == 1 {
+		scope = ScopeTypeBidder
+		name = split[0]
+	} else {
+		return ScopedName{}, fmt.Errorf("unable to parse condition: %s", condition)
+	}
+
+	return ScopedName{
+		Scope: scope,
+		Name:  name,
+	}, nil
 }
diff --git a/privacy/policyenforcer.go b/privacy/policyenforcer.go
new file mode 100644
index 00000000..e70c0d3d
--- /dev/null
+++ b/privacy/policyenforcer.go
@@ -0,0 +1,45 @@
+package privacy
+
+// NOTE: Reanme this package. Will eventually replace in its entirety with Activites.
+
+// PolicyEnforcer determines if personally identifiable information (PII) should be removed or anonymized per the policy.
+type PolicyEnforcer interface {
+	// CanEnforce returns true when policy information is specifically provided by the publisher.
+	CanEnforce() bool
+
+	// ShouldEnforce returns true when the OpenRTB request should have personally identifiable
+	// information (PII) removed or anonymized per the policy.
+	ShouldEnforce(bidder string) bool
+}
+
+// NilPolicyEnforcer implements the PolicyEnforcer interface but will always return false.
+type NilPolicyEnforcer struct{}
+
+// CanEnforce is hardcoded to always return false.
+func (NilPolicyEnforcer) CanEnforce() bool {
+	return false
+}
+
+// ShouldEnforce is hardcoded to always return false.
+func (NilPolicyEnforcer) ShouldEnforce(bidder string) bool {
+	return false
+}
+
+// EnabledPolicyEnforcer decorates a PolicyEnforcer with an enabled flag.
+type EnabledPolicyEnforcer struct {
+	Enabled        bool
+	PolicyEnforcer PolicyEnforcer
+}
+
+// CanEnforce returns true when the PolicyEnforcer can enforce.
+func (p EnabledPolicyEnforcer) CanEnforce() bool {
+	return p.PolicyEnforcer.CanEnforce()
+}
+
+// ShouldEnforce returns true when the enforcer is enabled the PolicyEnforcer allows enforcement.
+func (p EnabledPolicyEnforcer) ShouldEnforce(bidder string) bool {
+	if p.Enabled {
+		return p.PolicyEnforcer.ShouldEnforce(bidder)
+	}
+	return false
+}
diff --git a/static/bidder-info/pwbid.yaml b/static/bidder-info/pwbid.yaml
index ac687273..a172ff39 100644
--- a/static/bidder-info/pwbid.yaml
+++ b/static/bidder-info/pwbid.yaml
@@ -1,4 +1,4 @@
-endpoint: "https://bid.pubwise.io/prebid"
+endpoint: "https://bidder.east2.pubwise.io/bid/pubwisedirect"
 maintainer:
   email: info@pubwise.io
 gvlVendorID: 842
@@ -17,4 +17,4 @@ userSync:
   # PubWise supports user syncing, but requires configuration by the host. contact this
   # bidder directly at the email address in this file to ask about enabling user sync.
   supports:
-    - redirect
\ No newline at end of file
+    - redirect


Test patch:
diff --git a/adapters/pwbid/pwbid_test.go b/adapters/pwbid/pwbid_test.go
index 21a41942..194e4bde 100644
--- a/adapters/pwbid/pwbid_test.go
+++ b/adapters/pwbid/pwbid_test.go
@@ -10,7 +10,7 @@ import (
 
 func TestJsonSamples(t *testing.T) {
 	bidder, buildErr := Builder(openrtb_ext.BidderPWBid, config.Adapter{
-		Endpoint: "https://bid.pubwise.io/prebid"},
+		Endpoint: "https://bidder.east2.pubwise.io/bid/pubwisedirect"},
 		config.Server{ExternalUrl: "http://hosturl.com", GvlID: 842, DataCenter: "2"})
 
 	if buildErr != nil {
diff --git a/adapters/pwbid/pwbidtest/exemplary/banner.json b/adapters/pwbid/pwbidtest/exemplary/banner.json
index ba618cb8..4cf93e1a 100644
--- a/adapters/pwbid/pwbidtest/exemplary/banner.json
+++ b/adapters/pwbid/pwbidtest/exemplary/banner.json
@@ -23,7 +23,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://bid.pubwise.io/prebid",
+        "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect",
         "body": {
           "id": "test-request-id-banner",
           "imp": [
@@ -90,4 +90,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/adapters/pwbid/pwbidtest/exemplary/native.json b/adapters/pwbid/pwbidtest/exemplary/native.json
index 907c16d4..ff57752c 100644
--- a/adapters/pwbid/pwbidtest/exemplary/native.json
+++ b/adapters/pwbid/pwbidtest/exemplary/native.json
@@ -18,7 +18,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://bid.pubwise.io/prebid",
+        "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect",
         "body": {
           "id": "test-request-id-native",
           "imp": [
@@ -78,4 +78,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/adapters/pwbid/pwbidtest/exemplary/optional-params.json b/adapters/pwbid/pwbidtest/exemplary/optional-params.json
index a080be90..5ababb24 100644
--- a/adapters/pwbid/pwbidtest/exemplary/optional-params.json
+++ b/adapters/pwbid/pwbidtest/exemplary/optional-params.json
@@ -25,7 +25,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://bid.pubwise.io/prebid",
+        "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect",
         "body": {
           "id": "test-request-id-banner",
           "imp": [
@@ -94,4 +94,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/adapters/pwbid/pwbidtest/exemplary/video.json b/adapters/pwbid/pwbidtest/exemplary/video.json
index b74c780d..6257de63 100644
--- a/adapters/pwbid/pwbidtest/exemplary/video.json
+++ b/adapters/pwbid/pwbidtest/exemplary/video.json
@@ -20,7 +20,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://bid.pubwise.io/prebid",
+        "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/pwbid/pwbidtest/supplemental/response-200-without-body.json b/adapters/pwbid/pwbidtest/supplemental/response-200-without-body.json
index 146ba93a..0d469893 100644
--- a/adapters/pwbid/pwbidtest/supplemental/response-200-without-body.json
+++ b/adapters/pwbid/pwbidtest/supplemental/response-200-without-body.json
@@ -19,7 +19,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://bid.pubwise.io/prebid",
+        "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/pwbid/pwbidtest/supplemental/response-204.json b/adapters/pwbid/pwbidtest/supplemental/response-204.json
index 5fff7ee3..4fc8961e 100644
--- a/adapters/pwbid/pwbidtest/supplemental/response-204.json
+++ b/adapters/pwbid/pwbidtest/supplemental/response-204.json
@@ -19,7 +19,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://bid.pubwise.io/prebid",
+        "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/pwbid/pwbidtest/supplemental/response-400.json b/adapters/pwbid/pwbidtest/supplemental/response-400.json
index d594e571..a1517883 100644
--- a/adapters/pwbid/pwbidtest/supplemental/response-400.json
+++ b/adapters/pwbid/pwbidtest/supplemental/response-400.json
@@ -19,7 +19,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://bid.pubwise.io/prebid",
+        "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/pwbid/pwbidtest/supplemental/response-500.json b/adapters/pwbid/pwbidtest/supplemental/response-500.json
index fa3d4d06..c2b56494 100644
--- a/adapters/pwbid/pwbidtest/supplemental/response-500.json
+++ b/adapters/pwbid/pwbidtest/supplemental/response-500.json
@@ -19,7 +19,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "https://bid.pubwise.io/prebid",
+        "uri": "https://bidder.east2.pubwise.io/bid/pubwisedirect",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index d1b6622f..973edc53 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -17,6 +17,7 @@ import (
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/privacy"
 	"github.com/prebid/prebid-server/util/ptrutil"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
@@ -4273,3 +4274,72 @@ func TestGetMediaTypeForBid(t *testing.T) {
 		})
 	}
 }
+
+func TemporarilyDisabledTestCleanOpenRTBRequestsActivitiesFetchBids(t *testing.T) {
+	testCases := []struct {
+		name              string
+		req               *openrtb2.BidRequest
+		componentName     string
+		allow             bool
+		expectedReqNumber int
+	}{
+		{
+			name:              "request_with_one_bidder_allowed",
+			req:               newBidRequest(t),
+			componentName:     "appnexus",
+			allow:             true,
+			expectedReqNumber: 1,
+		},
+		{
+			name:              "request_with_one_bidder_not_allowed",
+			req:               newBidRequest(t),
+			componentName:     "appnexus",
+			allow:             false,
+			expectedReqNumber: 0,
+		},
+	}
+
+	for _, test := range testCases {
+		privacyConfig := getDefaultActivityConfig(test.componentName, test.allow)
+		activities, err := privacy.NewActivityControl(privacyConfig)
+		assert.NoError(t, err, "")
+		auctionReq := AuctionRequest{
+			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: test.req},
+			UserSyncs:         &emptyUsersync{},
+			Activities:        activities,
+		}
+
+		bidderToSyncerKey := map[string]string{}
+		reqSplitter := &requestSplitter{
+			bidderToSyncerKey: bidderToSyncerKey,
+			me:                &metrics.MetricsEngineMock{},
+			hostSChainNode:    nil,
+			bidderInfo:        config.BidderInfos{},
+		}
+
+		t.Run(test.name, func(t *testing.T) {
+			bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo)
+			assert.Empty(t, errs)
+			assert.Len(t, bidderRequests, test.expectedReqNumber)
+		})
+	}
+}
+
+func getDefaultActivityConfig(componentName string, allow bool) *config.AccountPrivacy {
+	return &config.AccountPrivacy{
+		AllowActivities: config.AllowActivities{
+			FetchBids: config.Activity{
+				Default: ptrutil.ToPtr(true),
+				Rules: []config.ActivityRule{
+					{
+						Allow: allow,
+						Condition: config.ActivityCondition{
+							ComponentName: []string{componentName},
+							ComponentType: []string{"bidder"},
+						},
+					},
+				},
+			},
+		},
+	}
+}
diff --git a/privacy/enforcer_test.go b/privacy/enforcer_test.go
index b0c4032c..e87a9eb2 100644
--- a/privacy/enforcer_test.go
+++ b/privacy/enforcer_test.go
@@ -1,18 +1,427 @@
 package privacy
 
 import (
-	"testing"
-
+	"errors"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/util/ptrutil"
 	"github.com/stretchr/testify/assert"
+	"testing"
 )
 
-func TestNilEnforcerCanEnforce(t *testing.T) {
-	nilEnforcer := &NilPolicyEnforcer{}
-	assert.False(t, nilEnforcer.CanEnforce())
+func TemporarilyDisabledTestNewActivityControl(t *testing.T) {
+
+	testCases := []struct {
+		name            string
+		privacyConf     *config.AccountPrivacy
+		activityControl ActivityControl
+		err             error
+	}{
+		{
+			name:            "privacy_config_is_nil",
+			privacyConf:     nil,
+			activityControl: ActivityControl{plans: nil},
+			err:             nil,
+		},
+		{
+			name: "privacy_config_is_specified_and_correct",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					SyncUser:                 getDefaultActivityConfig(),
+					FetchBids:                getDefaultActivityConfig(),
+					EnrichUserFPD:            getDefaultActivityConfig(),
+					ReportAnalytics:          getDefaultActivityConfig(),
+					TransmitUserFPD:          getDefaultActivityConfig(),
+					TransmitPreciseGeo:       getDefaultActivityConfig(),
+					TransmitUniqueRequestIds: getDefaultActivityConfig(),
+					TransmitTids:             getDefaultActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: map[Activity]ActivityPlan{
+				ActivitySyncUser:                 getDefaultActivityPlan(),
+				ActivityFetchBids:                getDefaultActivityPlan(),
+				ActivityEnrichUserFPD:            getDefaultActivityPlan(),
+				ActivityReportAnalytics:          getDefaultActivityPlan(),
+				ActivityTransmitUserFPD:          getDefaultActivityPlan(),
+				ActivityTransmitPreciseGeo:       getDefaultActivityPlan(),
+				ActivityTransmitUniqueRequestIds: getDefaultActivityPlan(),
+				ActivityTransmitTids:             getDefaultActivityPlan(),
+			}},
+			err: nil,
+		},
+		{
+			name: "privacy_config_is_specified_and_SyncUser_is_incorrect",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					SyncUser: getIncorrectActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: nil},
+			err:             errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+		{
+			name: "privacy_config_is_specified_and_FetchBids_is_incorrect",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					FetchBids: getIncorrectActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: nil},
+			err:             errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+		{
+			name: "privacy_config_is_specified_and_EnrichUserFPD_is_incorrect",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					EnrichUserFPD: getIncorrectActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: nil},
+			err:             errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+		{
+			name: "privacy_config_is_specified_and_ReportAnalytics_is_incorrect",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					ReportAnalytics: getIncorrectActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: nil},
+			err:             errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+		{
+			name: "privacy_config_is_specified_and_TransmitUserFPD_is_incorrect",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					TransmitUserFPD: getIncorrectActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: nil},
+			err:             errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+		{
+			name: "privacy_config_is_specified_and_TransmitPreciseGeo_is_incorrect",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					TransmitPreciseGeo: getIncorrectActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: nil},
+			err:             errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+		{
+			name: "privacy_config_is_specified_and_TransmitUniqueRequestIds_is_incorrect",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					TransmitUniqueRequestIds: getIncorrectActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: nil},
+			err:             errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+		{
+			name: "privacy_config_is_specified_and_TransmitTids_is_incorrect",
+			privacyConf: &config.AccountPrivacy{
+				AllowActivities: config.AllowActivities{
+					TransmitTids: getIncorrectActivityConfig(),
+				},
+			},
+			activityControl: ActivityControl{plans: nil},
+			err:             errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			actualAC, actualErr := NewActivityControl(test.privacyConf)
+			if test.err == nil {
+				assert.Equal(t, test.activityControl, actualAC)
+				assert.NoError(t, actualErr)
+			} else {
+				assert.EqualError(t, actualErr, test.err.Error())
+			}
+		})
+	}
+}
+
+func TestActivityDefaultToDefaultResult(t *testing.T) {
+
+	testCases := []struct {
+		name            string
+		activityDefault *bool
+		expectedResult  ActivityResult
+	}{
+		{
+			name:            "activityDefault_is_nil",
+			activityDefault: nil,
+			expectedResult:  ActivityAllow,
+		},
+		{
+			name:            "activityDefault_is_true",
+			activityDefault: ptrutil.ToPtr(true),
+			expectedResult:  ActivityAllow,
+		},
+		{
+			name:            "activityDefault_is_false",
+			activityDefault: ptrutil.ToPtr(false),
+			expectedResult:  ActivityDeny,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			actualResult := activityDefaultToDefaultResult(test.activityDefault)
+			assert.Equal(t, test.expectedResult, actualResult)
+		})
+	}
+}
+
+func TestAllowActivityControl(t *testing.T) {
+
+	testCases := []struct {
+		name            string
+		activityControl ActivityControl
+		activity        Activity
+		target          ScopedName
+		activityResult  ActivityResult
+	}{
+		{
+			name:            "plans_is_nil",
+			activityControl: ActivityControl{plans: nil},
+			activity:        ActivityFetchBids,
+			target:          ScopedName{Scope: "bidder", Name: "bidderA"},
+			activityResult:  ActivityAbstain,
+		},
+		{
+			name: "activity_not_defined",
+			activityControl: ActivityControl{plans: map[Activity]ActivityPlan{
+				ActivitySyncUser: getDefaultActivityPlan()}},
+			activity:       ActivityFetchBids,
+			target:         ScopedName{Scope: "bidder", Name: "bidderA"},
+			activityResult: ActivityAbstain,
+		},
+		{
+			name: "activity_defined_but_not_found_default_returned",
+			activityControl: ActivityControl{plans: map[Activity]ActivityPlan{
+				ActivityFetchBids: getDefaultActivityPlan()}},
+			activity:       ActivityFetchBids,
+			target:         ScopedName{Scope: "bidder", Name: "bidderB"},
+			activityResult: ActivityAllow,
+		},
+		{
+			name: "activity_defined_and_allowed",
+			activityControl: ActivityControl{plans: map[Activity]ActivityPlan{
+				ActivityFetchBids: getDefaultActivityPlan()}},
+			activity:       ActivityFetchBids,
+			target:         ScopedName{Scope: "bidder", Name: "bidderA"},
+			activityResult: ActivityAllow,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			actualResult := test.activityControl.Allow(test.activity, test.target)
+			assert.Equal(t, test.activityResult, actualResult)
+
+		})
+	}
+}
+
+func TestAllowComponentEnforcementRule(t *testing.T) {
+
+	testCases := []struct {
+		name           string
+		componentRule  ComponentEnforcementRule
+		target         ScopedName
+		activityResult ActivityResult
+	}{
+		{
+			name: "activity_is_allowed",
+			componentRule: ComponentEnforcementRule{
+				allowed: true,
+				componentName: []ScopedName{
+					{Scope: "bidder", Name: "bidderA"},
+				},
+				componentType: []string{"bidder"},
+			},
+			target:         ScopedName{Scope: "bidder", Name: "bidderA"},
+			activityResult: ActivityAllow,
+		},
+		{
+			name: "activity_is_not_allowed",
+			componentRule: ComponentEnforcementRule{
+				allowed: false,
+				componentName: []ScopedName{
+					{Scope: "bidder", Name: "bidderA"},
+				},
+				componentType: []string{"bidder"},
+			},
+			target:         ScopedName{Scope: "bidder", Name: "bidderA"},
+			activityResult: ActivityDeny,
+		},
+		{
+			name: "abstain_both_clauses_do_not_match",
+			componentRule: ComponentEnforcementRule{
+				allowed: true,
+				componentName: []ScopedName{
+					{Scope: "bidder", Name: "bidderA"},
+				},
+				componentType: []string{"bidder"},
+			},
+			target:         ScopedName{Scope: "bidder", Name: "bidderB"},
+			activityResult: ActivityAbstain,
+		},
+		{
+			name: "activity_is_not_allowed_componentName_only",
+			componentRule: ComponentEnforcementRule{
+				allowed: true,
+				componentName: []ScopedName{
+					{Scope: "bidder", Name: "bidderA"},
+				},
+			},
+			target:         ScopedName{Scope: "bidder", Name: "bidderA"},
+			activityResult: ActivityAllow,
+		},
+		{
+			name: "activity_is_allowed_componentType_only",
+			componentRule: ComponentEnforcementRule{
+				allowed:       true,
+				componentType: []string{"bidder"},
+			},
+			target:         ScopedName{Scope: "bidder", Name: "bidderB"},
+			activityResult: ActivityAllow,
+		},
+		{
+			name: "abstain_activity_no_componentType_and_no_componentName",
+			componentRule: ComponentEnforcementRule{
+				allowed: true,
+			},
+			target:         ScopedName{Scope: "bidder", Name: "bidderB"},
+			activityResult: ActivityAbstain,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			actualResult := test.componentRule.Allow(test.target)
+			assert.Equal(t, test.activityResult, actualResult)
+
+		})
+	}
+}
+
+func TestNewScopedName(t *testing.T) {
+
+	testCases := []struct {
+		name              string
+		condition         string
+		expectedScopeName ScopedName
+		err               error
+	}{
+		{
+			name:              "condition_is_empty",
+			condition:         "",
+			expectedScopeName: ScopedName{},
+			err:               errors.New("unable to parse empty condition"),
+		},
+		{
+			name:              "condition_is_incorrect",
+			condition:         "bidder.bidderA.bidderB",
+			expectedScopeName: ScopedName{},
+			err:               errors.New("unable to parse condition: bidder.bidderA.bidderB"),
+		},
+		{
+			name:              "condition_is_scoped_to_bidder",
+			condition:         "bidder.bidderA",
+			expectedScopeName: ScopedName{Scope: "bidder", Name: "bidderA"},
+			err:               nil,
+		},
+		{
+			name:              "condition_is_scoped_to_analytics",
+			condition:         "analytics.bidderA",
+			expectedScopeName: ScopedName{Scope: "analytics", Name: "bidderA"},
+			err:               nil,
+		},
+		{
+			name:              "condition_is_scoped_to_userid",
+			condition:         "userid.bidderA",
+			expectedScopeName: ScopedName{Scope: "userid", Name: "bidderA"},
+			err:               nil,
+		},
+		{
+			name:              "condition_is_bidder_name",
+			condition:         "bidderA",
+			expectedScopeName: ScopedName{Scope: "bidder", Name: "bidderA"},
+			err:               nil,
+		},
+		{
+			name:              "condition_is_module_tag_rtd",
+			condition:         "rtd.test",
+			expectedScopeName: ScopedName{Scope: "rtd", Name: "test"},
+			err:               nil,
+		},
+		{
+			name:              "condition_scope_defaults_to_genera",
+			condition:         "test.test",
+			expectedScopeName: ScopedName{Scope: "general", Name: "test"},
+			err:               nil,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			actualSN, actualErr := NewScopedName(test.condition)
+			if test.err == nil {
+				assert.Equal(t, test.expectedScopeName, actualSN)
+				assert.NoError(t, actualErr)
+			} else {
+				assert.EqualError(t, actualErr, test.err.Error())
+			}
+		})
+	}
+}
+
+// constants
+func getDefaultActivityConfig() config.Activity {
+	return config.Activity{
+		Default: ptrutil.ToPtr(true),
+		Rules: []config.ActivityRule{
+			{
+				Allow: true,
+				Condition: config.ActivityCondition{
+					ComponentName: []string{"bidderA"},
+					ComponentType: []string{"bidder"},
+				},
+			},
+		},
+	}
+}
+
+func getDefaultActivityPlan() ActivityPlan {
+	return ActivityPlan{
+		defaultResult: ActivityAllow,
+		rules: []ActivityRule{
+			ComponentEnforcementRule{
+				allowed: true,
+				componentName: []ScopedName{
+					{Scope: "bidder", Name: "bidderA"},
+				},
+				componentType: []string{"bidder"},
+			},
+		},
+	}
 }
 
-func TestNilEnforcerShouldEnforce(t *testing.T) {
-	nilEnforcer := &NilPolicyEnforcer{}
-	assert.False(t, nilEnforcer.ShouldEnforce(""))
-	assert.False(t, nilEnforcer.ShouldEnforce("anyBidder"))
+func getIncorrectActivityConfig() config.Activity {
+	return config.Activity{
+		Default: ptrutil.ToPtr(true),
+		Rules: []config.ActivityRule{
+			{
+				Allow: true,
+				Condition: config.ActivityCondition{
+					ComponentName: []string{"bidder.bidderA.bidderB"},
+					ComponentType: []string{"bidder"},
+				},
+			},
+		},
+	}
 }
diff --git a/privacy/policyenforcer_test.go b/privacy/policyenforcer_test.go
new file mode 100644
index 00000000..b0c4032c
--- /dev/null
+++ b/privacy/policyenforcer_test.go
@@ -0,0 +1,18 @@
+package privacy
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNilEnforcerCanEnforce(t *testing.T) {
+	nilEnforcer := &NilPolicyEnforcer{}
+	assert.False(t, nilEnforcer.CanEnforce())
+}
+
+func TestNilEnforcerShouldEnforce(t *testing.T) {
+	nilEnforcer := &NilPolicyEnforcer{}
+	assert.False(t, nilEnforcer.ShouldEnforce(""))
+	assert.False(t, nilEnforcer.ShouldEnforce("anyBidder"))
+}