File frizbee-0.1.7.obscpio of Package frizbee

07070100000000000081A400000000000000000000000167E26F59000000EF000000000000000000000000000000000000001B00000000frizbee-0.1.7/.frizbee.ymlghactions:
  exclude:
    # Exclude the SLSA GitHub Generator workflow.
    # See https://github.com/slsa-framework/slsa-github-generator/issues/2993
    - slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml
07070100000001000081A400000000000000000000000167E26F59000009C3000000000000000000000000000000000000001C00000000frizbee-0.1.7/.golangci.ymlrun:
  issues-exit-code: 1
  timeout: 5m

linters-settings:
  lll:
    line-length: 130
  gocyclo:
    min-complexity: 15
  gci:
    sections:
      - standard
      - default
      - prefix(github.com/stacklok/frizbee)
  revive:
    # see https://github.com/mgechev/revive#available-rules for details.
    ignore-generated-header: true
    severity: warning
    rules:
      - name: blank-imports
        severity: warning
      - name: context-as-argument
      - name: context-keys-type
      - name: duplicated-imports
      - name: error-naming
      # - name: error-strings #BDG: This was enabled for months, but it suddenly started working on 3/2/2022.. come to find out we have TONS of error messages starting with capital... disabling for now(ever?)
      - name: error-return
      - name: exported
        severity: error
      - name: if-return
      # - name: get-return // BDG: We have a lot of API endpoint handlers named like getFoos but write to response vs return... maybe later can figure that out
      - name: identical-branches
      - name: indent-error-flow
      - name: import-shadowing
      - name: package-comments
      - name: range-val-in-closure
      - name: range-val-address
      - name: redefines-builtin-id
      - name: struct-tag
      - name: unconditional-recursion
      - name: unnecessary-stmt
      - name: unreachable-code
      - name: unused-parameter
      - name: unused-receiver
      - name: unhandled-error
        disabled: true

linters:
  disable-all: true
  enable:
    - asasalint
    - asciicheck
    - bidichk
    - bodyclose
    - contextcheck
    - copyloopvar
    - decorder
    - dogsled
    - dupl
    - errcheck
    - errname
    - exhaustive
    - forbidigo
    - forcetypeassert
    - gci
    - gochecknoglobals
    - gochecknoinits
    - gochecksumtype
    - goconst
    - gocyclo
    - gofmt
    - goprintffuncname
    - gosec
    - gosimple
    - gosimple
    - govet
    - importas
    - inamedparam
    - ineffassign
    - interfacebloat
    - lll
    - makezero
    - mirror
    - noctx
    - nosprintfhostport
    - paralleltest
    - perfsprint
    - promlinter
    - revive
    - staticcheck
    - tenv
    - thelper
    - tparallel
    - unparam
    - unused
    - usestdlibvars

issues:
  exclude-use-default: false
  exclude-rules:
    - path: '(.+)_test\.go'
      linters:
        - lll

output:
  formats:
    - format: colored-line-number
  print-issued-lines: true
  print-linter-name: true
  sort-results: true
07070100000002000081A400000000000000000000000167E26F5900000B80000000000000000000000000000000000000001F00000000frizbee-0.1.7/.goreleaser.yaml# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
project_name: frizbee
version: 2
# This section defines the build matrix.
builds:
  - env:
      - GO111MODULE=on
      - CGO_ENABLED=0
    flags:
      - -trimpath
      - -tags=netgo
    ldflags:
      - "-s -w"
      - "-X main.Version={{ .Env.VERSION }}"
      - "-X main.Commit={{ .Env.COMMIT }}"
      - "-X main.CommitDate={{ .Env.COMMIT_DATE }}"
      - "-X main.TreeState={{ .Env.TREE_STATE }}"
      - "-X github.com/stacklok/frizbee/internal/cli.CLIVersion={{ .Env.VERSION }}"
    goos:
      - linux
      - windows
      - darwin
    goarch:
      - amd64
      - arm64
    main: ./main.go
# This section defines the release format.
archives:
  - format: tar.gz # we can use binary, but it seems there's an issue where goreleaser skips the sboms
    name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
    format_overrides:
      - goos: windows
        format: zip
# This section defines how to release to winget.
# winget:
#   - name: frizbee
#     publisher: stacklok
#     license: Apache-2.0
#     license_url: "https://github.com/stacklok/frizbee/blob/main/LICENSE"
#     copyright: Stacklok, Inc.
#     homepage: https://stacklok.com
#     short_description: 'frizbee is a tool you may throw a tag at and it comes back with a checksum.'
#     publisher_support_url: "https://github.com/stacklok/frizbee/issues/new/choose"
#     package_identifier: "stacklok.frizbee"
#     url_template: "https://github.com/stacklok/frizbee/releases/download/{{ .Tag }}/{{ .ArtifactName }}"
#     skip_upload: auto
#     release_notes: "{{.Changelog}}"
#     tags:
#       - golang
#       - cli
#     commit_author:
#       name: stacklokbot
#       email: info@stacklok.com
#     goamd64: v1
#     repository:
#       owner: stacklok
#       name: winget-pkgs
#       branch: "frizbee-{{.Version}}"
#       token: "{{ .Env.WINGET_GITHUB_TOKEN }}"
#       pull_request:
#         enabled: true
#         draft: false
#         base:
#           owner: microsoft
#           name: winget-pkgs
#           branch: master
# This section defines whether we want to release the source code too.
source:
  enabled: true
# This section defines how to generate the changelog
changelog:
  sort: asc
  use: github
# This section defines for which artifact types to generate SBOMs.
sboms:
  - artifacts: archive
# This section defines the release policy.
release:
  github:
    owner: stacklok
    name: frizbee
# This section defines how and which artifacts we want to sign for the release.
signs:
  - cmd: cosign
    args:
      - "sign-blob"
      - "--output-signature=${signature}"
      - "--output-certificate=${certificate}"
      - "${artifact}"
      - "--yes" # needed on cosign 2.0.0+
    artifacts: archive
    output: true
    certificate: '{{ trimsuffix (trimsuffix .Env.artifact ".zip") ".tar.gz" }}.pem'
07070100000003000081A400000000000000000000000167E26F5900000C93000000000000000000000000000000000000002100000000frizbee-0.1.7/CODE_OF_CONDUCT.md# Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or
  advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
  address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at <code-of-conduct@stacklok.com>. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]

[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
07070100000004000081A400000000000000000000000167E26F5900000EA0000000000000000000000000000000000000001E00000000frizbee-0.1.7/CONTRIBUTING.md
# Contributing to Frizbee
First off, thank you for taking the time to contribute to Frizbee! :+1: :tada: Frizbee is released under the Apache 2.0 license. If you would like to contribute something or want to hack on the code, this document should help you get started. You can find some hints for starting development in Frizbee's  [README](https://github.com/stacklok/frizbee/blob/main/README.md).

## Table of contents 
- [Code of Conduct](#code-of-conduct)
- [Reporting Security Vulnerabilities](#reporting-security-vulnerabilities)
- [How to Contribute](#how-to-contribute)
  - [Sign the Contributor License Agreement](#sign-the-contributor-license-agreement)
  - [Using GitHub Issues](#using-github-issues)
  - [Not sure how to start contributing...](#not-sure-how-to-start-contributing)
  - [Pull Request Process](#pull-request-process)
  - [Contributing to docs](#contributing-to-docs)
  - [Commit Message Guidelines](#commit-message-guidelines)
  

## Code of Conduct
This project adheres to the [Contributor Covenant](https://github.com/stacklok/frizbee/blob/main/CODE_OF_CONDUCT.md) code of conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to code-of-conduct@stacklok.dev.

## Reporting Security Vulnerabilities

If you think you have found a security vulnerability in Frizbee please DO NOT disclose it publicly until we’ve had a chance to fix it. Please don’t report security vulnerabilities using GitHub issues; instead, please follow this [process](https://github.com/stacklok/frizbee/blob/main/SECURITY.md)

## How to Contribute

### Using GitHub Issues
We use GitHub issues to track bugs and enhancements. If you have a general usage question, please ask in [Frizbee's discussion forum](https://discord.com/invite/RkzVuTp3WK). 

If you are reporting a bug, please help to speed up problem diagnosis by providing as much information as possible. Ideally, that would include a small sample project that reproduces the problem.

### Sign the Contributor License Agreement
Before we accept a non-trivial patch or pull request, we will need you to sign the [Contributor License Agreement](https://cla-assistant.io/stacklok/frizbee). Signing the contributor’s agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. Active contributors might be asked to join the core team and given the ability to merge pull requests.

### Not sure how to start contributing...
PRs to resolve existing issues are greatly appreciated and issues labeled as ["good first issue"](https://github.com/stacklok/frizbee/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) are a great place to start!

### Pull Request Process
* Create an issue outlining the fix or feature.
* Fork the Frizbee repository to your own GitHub account and clone it locally.
* Hack on your changes.
* Correctly format your commit messages, see [Commit Message Guidelines](#Commit-Message-Guidelines) below.
* Open a PR by ensuring the title and its description reflect the content of the PR.
* Ensure that CI passes, if it fails, fix the failures.
* Every pull request requires a review from the core Frizbee team before merging.
* Once approved, all of your commits will be squashed into a single commit with your PR title.

### Contributing to docs
Follow [this guide](https://github.com/stacklok/frizbee/blob/main/docs/README.md) for instructions on building, running, and previewing Miner's documentation.

### Commit Message Guidelines
We follow the commit formatting recommendations found on [Chris Beams' How to Write a Git Commit Message article](https://chris.beams.io/posts/git-commit/).
07070100000005000081A400000000000000000000000167E26F5900002C50000000000000000000000000000000000000001600000000frizbee-0.1.7/LICENSE                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright 2023 Stacklok, Inc.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
07070100000006000081A400000000000000000000000167E26F59000019FC000000000000000000000000000000000000001800000000frizbee-0.1.7/README.md![image](https://github.com/stacklok/frizbee/assets/16540482/35034046-d962-475d-b8e2-67b7625f2a60)

---
[![Coverage Status](https://coveralls.io/repos/github/stacklok/frizbee/badge.svg?branch=main)](https://coveralls.io/github/stacklok/frizbee?branch=main) | [![License: Apache 2.0](https://img.shields.io/badge/License-Apache2.0-brightgreen.svg)](https://opensource.org/licenses/Apache-2.0) | [![](https://dcbadge.vercel.app/api/server/RkzVuTp3WK?logo=discord&label=Discord&color=5865&style=flat)](https://discord.gg/RkzVuTp3WK)

---
# Frizbee

Frizbee is a tool you may throw a tag at and it comes back with a checksum.

It's a command-line tool designed to provide checksums for GitHub Actions
and container images based on tags.

It also includes a set of libraries for working with tags and checksums.

Frizbee is available as a GitHub Action: [frizbee-action](https://github.com/marketplace/actions/frizbee-action)

## Table of Contents

- [Installation](#installation)
- [Usage - CLI](#usage---cli)
  - [GitHub Actions](#github-actions)
  - [Container Images](#container-images)
- [Usage - Library](#usage---library)
  - [GitHub Actions](#github-actions)
  - [Container Images](#container-images)
- [Configuration](#configuration)
- [Contributing](#contributing)
- [License](#license)

## Installation

To install Frizbee, you can use the following methods:

```bash
# Using Go
go install github.com/stacklok/frizbee@v0.1.2
# add the path to your .bashrc
export PATH=$PATH:$(go env GOPATH)/bin



# Using Homebrew
brew install frizbee

# Using winget
winget install stacklok.frizbee
```

## Usage - CLI

### GitHub Actions

Frizbee can be used to generate checksums for GitHub Actions. This is useful
for verifying that the contents of a GitHub Action have not changed.

To quickly replace the GitHub Action references for your project, you can use
the `actions` command:

```bash
frizbee actions path/to/your/repo/.github/workflows/
```

This will write all the replacements to the files in the directory provided.

Note that this command will only replace the `uses` field of the GitHub Action
references.

Note that this command supports dry-run mode, which will print the replacements
to stdout instead of writing them to the files.

It also supports exiting with a non-zero exit code if any replacements are found. 
This is handy for CI/CD pipelines.

If you want to generate the replacement for a single GitHub Action, you can use the
same command:

```bash
frizbee actions metal-toolbox/container-push/.github/workflows/container-push.yml@main
```

This is useful if you're developing and want to quickly test the replacement.

### Container Images

Frizbee can be used to generate checksums for container images. This is useful
for verifying that the contents of a container image have not changed. This works
for all yaml/yml and Dockerfile fies in the directory provided by the `-d` flag.

To quickly replace the container image references for your project, you can use
the `image` command:

```bash
frizbee image path/to/your/yaml/files/
```

To get the digest for a single image tag, you can use the same command:

```bash
frizbee image ghcr.io/stacklok/minder/server:latest
```

This will print the image reference with the digest for the image tag provided.

## Usage - Library

Frizbee can also be used as a library. The library provides a set of functions
for working with tags and checksums. Here are a few examples of how you can use
the library:

### GitHub Actions

```go
// Create a new replacer
r := replacer.NewGitHubActionsReplacer(config.DefaultConfig())
...
// Parse a single GitHub Action reference
ret, err := r.ParseString(ctx, ghActionRef)
...
// Parse all GitHub Actions workflow yaml files in a given directory
res, err := r.ParsePath(ctx, dir)
...
// Parse and replace all GitHub Actions references in the provided file system
res, err := r.ParsePathInFS(ctx, bfs, base)
...
// Parse a single yaml file referencing GitHub Actions
res, err := r.ParseFile(ctx, fileHandler)
...
// List all GitHub Actions referenced in the given directory
res, err := r.ListPath(dir)
...
// List all GitHub Actions referenced in the provided file system
res, err := r.ListPathInFS(bfs, base)
...
// List all GitHub Actions referenced in the provided file
res, err := r.ListFile(fileHandler)
```

### Container images 

```go
// Create a new replacer
r := replacer.NewContainerImagesReplacer(config.DefaultConfig())
...
// Parse a single container image reference
ret, err := r.ParseString(ctx, ghActionRef)
...
// Parse all files containing container image references in a given directory
res, err := r.ParsePath(ctx, dir)
...
// Parse and replace all container image references in the provided file system
res, err := r.ParsePathInFS(ctx, bfs, base)
...
// Parse a single yaml file referencing container images
res, err := r.ParseFile(ctx, fileHandler)
...
// List all container images referenced in the given directory
res, err := r.ListPath(dir)
...
// List all container images referenced in the provided file system
res, err := r.ListPathInFS(bfs, base)
...
// List all container images referenced in the provided file
res, err := r.ListFile(fileHandler)
```

## Configuration

Frizbee can be configured by setting up a `.frizbee.yml` file. 
You can configure Frizbee to skip processing certain actions, i.e.

```yml
ghactions:
  exclude:
    # Exclude the SLSA GitHub Generator workflow.
    # See https://github.com/slsa-framework/slsa-github-generator/issues/2993
    - slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml

```

Similarly, you can exclude actions that are referenced using a particular branch:
```yml
ghactions:
  exclude_branches:
     - main
     - master
```
By default, Frizbee will exclude all actions that are referencing `main` or `master`.

You can also configure Frizbee to skip processing certain container images or certain tags:
```yml
images:
  exclude_images:
    - busybox
  exclude_tags:
    - devel
```
By default, Frizbee will exclude the image named `scratch` and the tag `latest`.

## Contributing & Community

Frizbee is maintained by a dedicated community of developers that want this open source project to benefit others and thrive. The main development of Frizbee is done in [Go](https://go.dev/). We welcome contributions of all types! Please see our [Contributing](./CONTRIBUTING.md) guide for more information on how you can help!

If you have questions, or just want to chat with us - please use the #frizbee channel on our [Discord Server](https://discord.gg/stacklok).

## License

Frizbee is licensed under the [Apache 2.0 License](./LICENSE).
07070100000007000081A400000000000000000000000167E26F5900000333000000000000000000000000000000000000001A00000000frizbee-0.1.7/SECURITY.md# Security Policy

## Reporting a Vulnerability

The Frizbee team and community take security seriously! We appreciate your efforts to disclose your findings responsibly and will make every effort to acknowledge your contributions.

To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/stacklok/frizbee/security/advisories/new) tab.

The Frizbee team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will inform you of the progress toward a fix and public disclosure. Throughout this process, we may ask for additional information or guidance to ensure we have fixed the issue.

Please report security bugs in third-party modules to the person or team maintaining the module. 
07070100000008000081A400000000000000000000000167E26F59000001F7000000000000000000000000000000000000001B00000000frizbee-0.1.7/Taskfile.yml---
version: '3'

tasks:
  default:
    silent: true
    cmds:
      - task -l
      
  test:
    desc: Run tests
    cmds:
      - go test -v ./...

  cover:
    desc: Run coverage
    cmds:
      - go test -coverprofile=coverage.out ./...

  build:
    desc: Build the frizbee binary
    cmds:
      - go build -ldflags="-X github.com/stacklok/frizbee/pkg/constants.CLIVersion=dev" -o ./bin/ ./...

  lint:
    desc: Run linter
    cmds:
      - golangci-lint run --timeout 5m0s --config .golangci.yml07070100000009000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001200000000frizbee-0.1.7/cmd0707010000000A000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001A00000000frizbee-0.1.7/cmd/actions0707010000000B000081A400000000000000000000000167E26F5900000C2B000000000000000000000000000000000000002500000000frizbee-0.1.7/cmd/actions/actions.go//
// Copyright 2024 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package actions provides command-line utilities to work with GitHub Actions.
package actions

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"

	"github.com/spf13/cobra"

	"github.com/stacklok/frizbee/internal/cli"
	"github.com/stacklok/frizbee/pkg/interfaces"
	"github.com/stacklok/frizbee/pkg/replacer"
	"github.com/stacklok/frizbee/pkg/utils/config"
)

// CmdGHActions represents the actions command
func CmdGHActions() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "actions",
		Short: "Replace tags in GitHub Actions workflows",
		Long: `This utility replaces tag or branch references in GitHub Actions workflows
with the latest commit hash of the referenced tag or branch.

Example:

	$ frizbee actions <.github/workflows> or <actions/checkout@v4>

This will replace all tag or branch references in all GitHub Actions workflows
for the given directory. Supports both directories and single references.

` + cli.TokenHelpText + "\n",
		Aliases:      []string{"ghactions"}, // backwards compatibility
		RunE:         replaceCmd,
		SilenceUsage: true,
		Args:         cobra.MaximumNArgs(1),
	}

	// flags
	cli.DeclareFrizbeeFlags(cmd, false)

	// sub-commands
	cmd.AddCommand(CmdList())

	return cmd
}

// nolint:errcheck
func replaceCmd(cmd *cobra.Command, args []string) error {
	// Set the default directory if not provided
	pathOrRef := ".github/workflows"
	if len(args) > 0 {
		pathOrRef = args[0]
	}

	// Extract the CLI flags from the cobra command
	cliFlags, err := cli.NewHelper(cmd)
	if err != nil {
		return err
	}

	// Set up the config
	cfg, err := config.FromCommand(cmd)
	if err != nil {
		return err
	}

	// Create a new replacer
	r := replacer.NewGitHubActionsReplacer(cfg).
		WithUserRegex(cliFlags.Regex).
		WithGitHubClientFromToken(os.Getenv(cli.GitHubTokenEnvKey))

	if cli.IsPath(pathOrRef) {
		dir := filepath.Clean(pathOrRef)
		// Replace the tags in the given directory
		res, err := r.ParsePath(cmd.Context(), dir)
		if err != nil {
			return err
		}

		if err := cliFlags.CheckModified(res.Modified); err != nil {
			return err
		}

		// Process the output files
		return cliFlags.ProcessOutput(dir, res.Processed, res.Modified)
	}
	// Replace the passed reference
	res, err := r.ParseString(cmd.Context(), pathOrRef)
	if err != nil {
		if errors.Is(err, interfaces.ErrReferenceSkipped) {
			fmt.Fprintln(cmd.OutOrStdout(), pathOrRef) // nolint:errcheck
			return nil
		}
		return err
	}
	fmt.Fprintf(cmd.OutOrStdout(), "%s@%s\n", res.Name, res.Ref) // nolint:errcheck
	return nil
}
0707010000000C000081A400000000000000000000000167E26F5900000AAE000000000000000000000000000000000000002200000000frizbee-0.1.7/cmd/actions/list.go//
// Copyright 2024 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package actions

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strconv"

	"github.com/olekukonko/tablewriter"
	"github.com/spf13/cobra"

	"github.com/stacklok/frizbee/internal/cli"
	"github.com/stacklok/frizbee/pkg/replacer"
	"github.com/stacklok/frizbee/pkg/utils/config"
)

// CmdList represents the one sub-command
func CmdList() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "list",
		Short: "Lists the used github actions",
		Long: `This utility lists all the github actions used in the workflows

Example: 
	frizbee action list .github/workflows
`,
		Aliases:      []string{"ls"},
		RunE:         list,
		SilenceUsage: true,
		Args:         cobra.MaximumNArgs(1),
	}

	cli.DeclareFrizbeeFlags(cmd, true)

	return cmd
}

func list(cmd *cobra.Command, args []string) error {
	// Set the default directory if not provided
	dir := ".github/workflows"
	if len(args) > 0 {
		dir = args[0]
	}

	dir = filepath.Clean(dir)
	if !cli.IsPath(dir) {
		return errors.New("the provided argument is not a path")
	}
	// Extract the CLI flags from the cobra command
	cliFlags, err := cli.NewHelper(cmd)
	if err != nil {
		return err
	}

	// Set up the config
	cfg, err := config.FromCommand(cmd)
	if err != nil {
		return err
	}

	// Create a new replacer
	r := replacer.NewGitHubActionsReplacer(cfg).
		WithUserRegex(cliFlags.Regex).
		WithGitHubClientFromToken(os.Getenv(cli.GitHubTokenEnvKey))

	// List the references in the directory
	res, err := r.ListPath(dir)
	if err != nil {
		return err
	}

	output := cmd.Flag("output").Value.String()
	switch output {
	case "json":
		jsonBytes, err := json.MarshalIndent(res.Entities, "", "  ")
		if err != nil {
			return err
		}
		jsonString := string(jsonBytes)

		fmt.Fprintln(cmd.OutOrStdout(), jsonString) // nolint:errcheck
		return nil
	case "table":
		table := tablewriter.NewWriter(cmd.OutOrStdout())
		table.SetHeader([]string{"No", "Type", "Name", "Ref"})
		for i, a := range res.Entities {
			table.Append([]string{strconv.Itoa(i + 1), a.Type, a.Name, a.Ref})
		}
		table.Render()
		return nil
	default:
		return fmt.Errorf("unknown output format: %s", output)
	}
}
0707010000000D000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001800000000frizbee-0.1.7/cmd/image0707010000000E000081A400000000000000000000000167E26F5900000AD4000000000000000000000000000000000000002100000000frizbee-0.1.7/cmd/image/image.go//
// Copyright 2024 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package image provides command-line utilities to work with container images.
package image

import (
	"errors"
	"fmt"
	"path/filepath"

	"github.com/spf13/cobra"

	"github.com/stacklok/frizbee/internal/cli"
	"github.com/stacklok/frizbee/pkg/interfaces"
	"github.com/stacklok/frizbee/pkg/replacer"
	"github.com/stacklok/frizbee/pkg/utils/config"
)

// CmdContainerImage represents the containers command
func CmdContainerImage() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "image",
		Short: "Replace container image references with checksums",
		Long: `This utility replaces tag or branch references in yaml/yml files
with the latest commit hash of the referenced tag or branch.
	
Example:

	$ frizbee image <path-to-yaml-files> or <ghcr.io/stacklok/minder/server:latest>

This will replace all tag or branch references in all yaml files for the given directory.
`,
		RunE:         replaceCmd,
		SilenceUsage: true,
		Aliases:      []string{"containerimage", "dockercompose", "compose"}, // backwards compatibility
		Args:         cobra.ExactArgs(1),
	}

	// flags
	cli.DeclareFrizbeeFlags(cmd, false)

	// sub-commands
	cmd.AddCommand(CmdList())

	return cmd
}

func replaceCmd(cmd *cobra.Command, args []string) error {
	// Extract the CLI flags from the cobra command
	cliFlags, err := cli.NewHelper(cmd)
	if err != nil {
		return err
	}

	// Set up the config
	cfg, err := config.FromCommand(cmd)
	if err != nil {
		return err
	}

	// Create a new replacer
	r := replacer.NewContainerImagesReplacer(cfg).
		WithUserRegex(cliFlags.Regex)

	if cli.IsPath(args[0]) {
		dir := filepath.Clean(args[0])
		// Replace the tags in the directory
		res, err := r.ParsePath(cmd.Context(), dir)
		if err != nil {
			return err
		}
		// Process the output files
		return cliFlags.ProcessOutput(dir, res.Processed, res.Modified)
	}
	// Replace the passed reference
	res, err := r.ParseString(cmd.Context(), args[0])
	if err != nil {
		if errors.Is(err, interfaces.ErrReferenceSkipped) {
			fmt.Fprintln(cmd.OutOrStdout(), args[0]) // nolint:errcheck
			return nil
		}
		return err
	}
	fmt.Fprintf(cmd.OutOrStdout(), "%s@%s\n", res.Name, res.Ref) // nolint:errcheck
	return nil

}
0707010000000F000081A400000000000000000000000167E26F59000009F9000000000000000000000000000000000000002000000000frizbee-0.1.7/cmd/image/list.go//
// Copyright 2024 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package image

import (
	"encoding/json"
	"errors"
	"fmt"
	"path/filepath"
	"strconv"

	"github.com/olekukonko/tablewriter"
	"github.com/spf13/cobra"

	"github.com/stacklok/frizbee/internal/cli"
	"github.com/stacklok/frizbee/pkg/replacer"
	"github.com/stacklok/frizbee/pkg/utils/config"
)

// CmdList represents the one sub-command
func CmdList() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "list",
		Short: "Lists the used container images",
		Long: `This utility lists all container images used in the files in the directory

Example: 
	frizbee image list <path>
`,
		Aliases:      []string{"ls"},
		RunE:         list,
		SilenceUsage: true,
		Args:         cobra.ExactArgs(1),
	}

	cli.DeclareFrizbeeFlags(cmd, true)

	return cmd
}

func list(cmd *cobra.Command, args []string) error {
	dir := filepath.Clean(args[0])
	if !cli.IsPath(dir) {
		return errors.New("the provided argument is not a path")
	}
	// Extract the CLI flags from the cobra command
	cliFlags, err := cli.NewHelper(cmd)
	if err != nil {
		return err
	}

	// Set up the config
	cfg, err := config.FromCommand(cmd)
	if err != nil {
		return err
	}

	// Create a new replacer
	r := replacer.NewContainerImagesReplacer(cfg).
		WithUserRegex(cliFlags.Regex)

	// List the references in the directory
	res, err := r.ListPath(dir)
	if err != nil {
		return err
	}

	output := cmd.Flag("output").Value.String()
	switch output {
	case "json":
		jsonBytes, err := json.MarshalIndent(res.Entities, "", "  ")
		if err != nil {
			return err
		}
		jsonString := string(jsonBytes)
		fmt.Fprintln(cmd.OutOrStdout(), jsonString) // nolint:errcheck
		return nil
	case "table":
		table := tablewriter.NewWriter(cmd.OutOrStdout())
		table.SetHeader([]string{"No", "Type", "Name", "Ref"})
		for i, a := range res.Entities {
			table.Append([]string{strconv.Itoa(i + 1), a.Type, a.Name, a.Ref})
		}
		table.Render()
		return nil
	default:
		return fmt.Errorf("unknown output format: %s", output)
	}
}
07070100000010000081A400000000000000000000000167E26F59000007D4000000000000000000000000000000000000001A00000000frizbee-0.1.7/cmd/root.go//
// Copyright 2023 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package cmd provides the frizbee command line interface.
package cmd

import (
	"context"
	"fmt"
	"os"

	"github.com/spf13/cobra"

	"github.com/stacklok/frizbee/cmd/actions"
	"github.com/stacklok/frizbee/cmd/image"
	"github.com/stacklok/frizbee/cmd/version"
	"github.com/stacklok/frizbee/pkg/utils/config"
)

// Execute runs the root command.
func Execute() {
	var rootCmd = &cobra.Command{
		Use:               "frizbee",
		Short:             "frizbee is a tool you may throw a tag at and it comes back with a checksum",
		PersistentPreRunE: prerun,
	}

	rootCmd.PersistentFlags().StringP("config", "c", ".frizbee.yml", "config file (default is .frizbee.yml)")

	rootCmd.AddCommand(actions.CmdGHActions())
	rootCmd.AddCommand(image.CmdContainerImage())
	rootCmd.AddCommand(version.CmdVersion())

	if err := rootCmd.ExecuteContext(context.Background()); err != nil {
		os.Exit(1)
	}
}

func prerun(cmd *cobra.Command, _ []string) error {
	ctx := cmd.Context()

	cfg, err := readConfig(cmd)
	if err != nil {
		return fmt.Errorf("failed to read config: %w", err)
	}

	ctx = context.WithValue(ctx, config.ContextConfigKey, cfg)

	cmd.SetContext(ctx)

	return nil
}

func readConfig(cmd *cobra.Command) (*config.Config, error) {
	configFile, err := cmd.Flags().GetString("config")
	if err != nil {
		return nil, fmt.Errorf("failed to get config file: %w", err)
	}

	return config.ParseConfigFile(configFile)
}
07070100000011000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001A00000000frizbee-0.1.7/cmd/version07070100000012000081A400000000000000000000000167E26F5900000440000000000000000000000000000000000000002500000000frizbee-0.1.7/cmd/version/version.go//
// Copyright 2024 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package version adds a version command.
package version

import (
	"github.com/spf13/cobra"

	"github.com/stacklok/frizbee/internal/cli"
)

// CmdVersion is the Cobra command for the version command.
func CmdVersion() *cobra.Command {
	return &cobra.Command{
		Use:   "version",
		Short: "Print frizbee CLI version",
		Long:  "The frizbee version command prints the version of the frizbee CLI.",
		Run: func(cmd *cobra.Command, _ []string) {
			cmd.Println(cli.VerboseCLIVersion)
		},
	}
}
07070100000013000081A400000000000000000000000167E26F5900000724000000000000000000000000000000000000001500000000frizbee-0.1.7/go.modmodule github.com/stacklok/frizbee

go 1.23.2
toolchain go1.24.1

require (
	github.com/deckarep/golang-set/v2 v2.8.0
	github.com/go-git/go-billy/v5 v5.6.2
	github.com/google/go-containerregistry v0.20.3
	github.com/google/go-github/v66 v66.0.0
	github.com/moby/buildkit v0.20.2
	github.com/olekukonko/tablewriter v0.0.5
	github.com/puzpuzpuz/xsync v1.5.2
	github.com/spf13/cobra v1.9.1
	github.com/stretchr/testify v1.10.0
	golang.org/x/sync v0.12.0
	gopkg.in/h2non/gock.v1 v1.1.2
	gopkg.in/yaml.v3 v3.0.1
)

require (
	github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
	github.com/containerd/typeurl/v2 v2.2.3 // indirect
	github.com/cyphar/filepath-securejoin v0.3.6 // indirect
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/docker/cli v27.5.1+incompatible // indirect
	github.com/docker/distribution v2.8.3+incompatible // indirect
	github.com/docker/docker-credential-helpers v0.8.2 // indirect
	github.com/gogo/protobuf v1.3.2 // indirect
	github.com/google/go-querystring v1.1.0 // indirect
	github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
	github.com/inconshreveable/mousetrap v1.1.0 // indirect
	github.com/klauspost/compress v1.17.11 // indirect
	github.com/mattn/go-runewidth v0.0.9 // indirect
	github.com/mitchellh/go-homedir v1.1.0 // indirect
	github.com/opencontainers/go-digest v1.0.0 // indirect
	github.com/opencontainers/image-spec v1.1.0 // indirect
	github.com/pkg/errors v0.9.1 // indirect
	github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	github.com/sirupsen/logrus v1.9.3 // indirect
	github.com/spf13/pflag v1.0.6 // indirect
	github.com/vbatts/tar-split v0.11.6 // indirect
	golang.org/x/sys v0.29.0 // indirect
	google.golang.org/protobuf v1.36.3 // indirect
)
07070100000014000081A400000000000000000000000167E26F5900002FEC000000000000000000000000000000000000001500000000frizbee-0.1.7/go.sumgithub.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/docker/cli v27.5.1+incompatible h1:JB9cieUT9YNiMITtIsguaN55PLOHhBSz3LKVc6cqWaY=
github.com/docker/cli v27.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M=
github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/moby/buildkit v0.20.2 h1:qIeR47eQ1tzI1rwz0on3Xx2enRw/1CKjFhoONVcTlMA=
github.com/moby/buildkit v0.20.2/go.mod h1:DhaF82FjwOElTftl0JUAJpH/SUIUx4UvcFncLeOtlDI=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/puzpuzpuz/xsync v1.5.2 h1:yRAP4wqSOZG+/4pxJ08fPTwrfL0IzE/LKQ/cw509qGY=
github.com/puzpuzpuz/xsync v1.5.2/go.mod h1:K98BYhX3k1dQ2M63t1YNVDanbwUPmBCAhNmVrrxfiGg=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
07070100000015000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001700000000frizbee-0.1.7/internal07070100000016000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001B00000000frizbee-0.1.7/internal/cli07070100000017000081A400000000000000000000000167E26F59000018B7000000000000000000000000000000000000002200000000frizbee-0.1.7/internal/cli/cli.go//
// Copyright 2024 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package cli provides utilities to work with the command-line interface.
package cli

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime/debug"
	"strings"
	"text/template"

	"github.com/go-git/go-billy/v5/osfs"
	"github.com/spf13/cobra"
)

const (
	// UserAgent is the user agent string used by frizbee.
	//
	// TODO (jaosorior): Add version information to this.
	UserAgent = "frizbee"
	// GitHubTokenEnvKey is the environment variable key for the GitHub token
	//nolint:gosec // This is not a hardcoded credential
	GitHubTokenEnvKey = "GITHUB_TOKEN"

	// TokenHelpText is the help text for the GitHub token
	TokenHelpText = "NOTE: It's recommended to set the " + GitHubTokenEnvKey +
		" environment variable given that GitHub has tighter rate limits on anonymous calls."
	verboseTemplate = `Version: {{ .Version }}
Go Version: {{.GoVersion}}
Git Commit: {{.Commit}}
Commit Date: {{.Time}}
OS/Arch: {{.OS}}/{{.Arch}}
Dirty: {{.Modified}}
`
)

// Helper is a common struct for implementing a CLI command that replaces
// files.
type Helper struct {
	DryRun        bool
	Quiet         bool
	ErrOnModified bool
	Regex         string
	Cmd           *cobra.Command
}

type versionInfo struct {
	Version   string
	GoVersion string
	Time      string
	Commit    string
	OS        string
	Arch      string
	Modified  bool
}

var (
	// CLIVersion is the version of the frizbee CLI.
	// nolint: gochecknoglobals
	CLIVersion = "dev"
	// VerboseCLIVersion is the verbose version of the frizbee CLI.
	// nolint: gochecknoglobals
	VerboseCLIVersion = ""
)

// nolint:gochecknoinits
func init() {
	buildinfo, ok := debug.ReadBuildInfo()
	if !ok {
		return
	}

	var vinfo versionInfo
	vinfo.Version = CLIVersion
	vinfo.GoVersion = buildinfo.GoVersion

	for _, kv := range buildinfo.Settings {
		switch kv.Key {
		case "vcs.time":
			vinfo.Time = kv.Value
		case "vcs.revision":
			vinfo.Commit = kv.Value
		case "vcs.modified":
			vinfo.Modified = kv.Value == "true"
		case "GOOS":
			vinfo.OS = kv.Value
		case "GOARCH":
			vinfo.Arch = kv.Value
		}
	}
	VerboseCLIVersion = vinfo.String()
}

func (vvs *versionInfo) String() string {
	stringBuilder := &strings.Builder{}
	tmpl := template.Must(template.New("version").Parse(verboseTemplate))
	err := tmpl.Execute(stringBuilder, vvs)
	if err != nil {
		panic(err)
	}
	return stringBuilder.String()
}

// NewHelper creates a new CLI Helper struct.
func NewHelper(cmd *cobra.Command) (*Helper, error) {
	dryRun, err := cmd.Flags().GetBool("dry-run")
	if err != nil {
		return nil, fmt.Errorf("failed to get dry-run flag: %w", err)
	}
	errOnModified, err := cmd.Flags().GetBool("error")
	if err != nil {
		return nil, fmt.Errorf("failed to get error flag: %w", err)
	}
	quiet, err := cmd.Flags().GetBool("quiet")
	if err != nil {
		return nil, fmt.Errorf("failed to get quiet flag: %w", err)
	}
	regex, err := cmd.Flags().GetString("regex")
	if err != nil {
		return nil, fmt.Errorf("failed to get regex flag: %w", err)
	}

	return &Helper{
		Cmd:           cmd,
		DryRun:        dryRun,
		ErrOnModified: errOnModified,
		Quiet:         quiet,
		Regex:         regex,
	}, nil
}

// DeclareFrizbeeFlags declares the flags common to all replacer commands.
func DeclareFrizbeeFlags(cmd *cobra.Command, enableOutput bool) {
	cmd.Flags().BoolP("dry-run", "n", false, "don't modify files")
	cmd.Flags().BoolP("quiet", "q", false, "don't print anything")
	cmd.Flags().BoolP("error", "e", false, "exit with error code if any file is modified")
	cmd.Flags().StringP("regex", "r", "", "regex to match artifact references")
	cmd.Flags().StringP("platform", "p", "", "platform to match artifact references, e.g. linux/amd64")
	if enableOutput {
		cmd.Flags().StringP("output", "o", "table", "output format. Can be 'json' or 'table'")
	}
}

// Logf logs the given message to the given command's stderr if the command is
// not quiet.
func (r *Helper) Logf(format string, args ...interface{}) {
	if !r.Quiet {
		fmt.Fprintf(r.Cmd.ErrOrStderr(), format, args...) // nolint:errcheck
	}
}

// CheckModified checks if any files were modified and returns an error if there were.
func (r *Helper) CheckModified(modified map[string]string) error {
	if len(modified) > 0 && r.ErrOnModified {
		if !r.Quiet {
			for path := range modified {
				r.Logf("Modified: %s\n", path)
			}
		}

		return errors.New("files were modified")
	}

	return nil
}

// ProcessOutput processes the given output files.
// If the command is quiet, the output is discarded.
// If the command is a dry run, the output is written to the command's stdout.
// Otherwise, the output is written to the given filesystem.
func (r *Helper) ProcessOutput(path string, processed []string, modified map[string]string) error {
	basedir := filepath.Dir(path)
	bfs := osfs.New(basedir, osfs.WithBoundOS())
	var out io.Writer
	for _, path := range processed {
		if !r.Quiet {
			r.Logf("Processed: %s\n", path)
		}
	}
	for path, content := range modified {
		if r.Quiet {
			out = io.Discard
		} else if r.DryRun {
			out = r.Cmd.OutOrStdout()
		} else {
			f, err := bfs.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0644)
			if err != nil {
				return fmt.Errorf("failed to open file %s: %w", path, err)
			}

			defer func() {
				if err := f.Close(); err != nil {
					fmt.Fprintf(r.Cmd.ErrOrStderr(), "failed to close file %s: %v", path, err) // nolint:errcheck
				}
			}()

			out = f
		}
		if !r.Quiet {
			r.Logf("Modified: %s\n", path)
		}
		_, err := fmt.Fprintf(out, "%s", content)
		if err != nil {
			return fmt.Errorf("failed to write to file %s: %w", path, err)
		}
	}

	return nil
}

// IsPath returns true if the given path is a file or directory.
func IsPath(pathOrRef string) bool {
	_, err := os.Stat(pathOrRef)
	return err == nil
}
07070100000018000081A400000000000000000000000167E26F5900001B8B000000000000000000000000000000000000002700000000frizbee-0.1.7/internal/cli/cli_test.gopackage cli

import (
	"path/filepath"
	"strings"
	"testing"

	"github.com/go-git/go-billy/v5"
	"github.com/go-git/go-billy/v5/memfs"
	"github.com/spf13/cobra"
	"github.com/stretchr/testify/assert"
)

func TestNewHelper(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name          string
		cmdArgs       []string
		expected      *Helper
		expectedError bool
	}{
		{
			name:    "ValidFlags",
			cmdArgs: []string{"--dry-run", "--quiet", "--error", "--regex", "test"},
			expected: &Helper{
				DryRun:        true,
				Quiet:         true,
				ErrOnModified: true,
				Regex:         "test",
			},
			expectedError: false,
		},
		{
			name:          "MissingFlags",
			cmdArgs:       []string{},
			expected:      &Helper{},
			expectedError: false,
		},
		{
			name:          "InvalidFlags",
			cmdArgs:       []string{"--nonexistent"},
			expected:      nil,
			expectedError: true,
		},
	}

	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			cmd := &cobra.Command{}
			DeclareFrizbeeFlags(cmd, true)
			cmd.SetArgs(tt.cmdArgs)

			if tt.expectedError {
				assert.Error(t, cmd.Execute())
				return
			}

			assert.NoError(t, cmd.Execute())

			helper, err := NewHelper(cmd)
			if tt.expectedError {
				assert.Error(t, err)
				assert.Nil(t, helper)
			} else {
				assert.NoError(t, err)
				assert.NotNil(t, helper)
				assert.Equal(t, tt.expected.DryRun, helper.DryRun)
				assert.Equal(t, tt.expected.Quiet, helper.Quiet)
				assert.Equal(t, tt.expected.ErrOnModified, helper.ErrOnModified)
				assert.Equal(t, tt.expected.Regex, helper.Regex)
			}
		})
	}
}

func TestCheckModified(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name         string
		helper       *Helper
		modified     map[string]string
		expectOutput string
		expectError  bool
	}{
		{
			name: "NoFilesModified",
			helper: &Helper{
				ErrOnModified: true,
				Quiet:         false,
				Cmd:           &cobra.Command{},
			},
			modified:     map[string]string{},
			expectOutput: "",
			expectError:  false,
		},
		{
			name: "FilesModifiedWithErrorFlag",
			helper: &Helper{
				ErrOnModified: true,
				Quiet:         false,
				Cmd:           &cobra.Command{},
			},
			modified: map[string]string{
				"file1.txt": "modified content",
				"file2.txt": "another modified content",
			},
			expectOutput: "Modified: file1.txt\nModified: file2.txt",
			expectError:  true,
		},
		{
			name: "FilesModifiedWithoutErrorFlag",
			helper: &Helper{
				ErrOnModified: false,
				Quiet:         false,
				Cmd:           &cobra.Command{},
			},
			modified: map[string]string{
				"file1.txt": "modified content",
			},
			expectOutput: "",
			expectError:  false,
		},
		{
			name: "FilesModifiedWithErrorFlagAndQuiet",
			helper: &Helper{
				ErrOnModified: true,
				Quiet:         true,
				Cmd:           &cobra.Command{},
			},
			modified: map[string]string{
				"file1.txt": "modified content",
			},
			expectOutput: "",
			expectError:  true,
		},
	}

	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			// Set up command error output
			var output strings.Builder
			tt.helper.Cmd.SetErr(&output)

			// Call the CheckModified method
			err := tt.helper.CheckModified(tt.modified)

			// Check if error status matches expectation
			if tt.expectError {
				assert.Error(t, err)
				assert.Equal(t, "files were modified", err.Error())
			} else {
				assert.NoError(t, err)
			}

			// Check if output contains expected strings
			if tt.expectOutput != "" {
				// We can't guarantee the order of map iteration, so we check that
				// the output contains each expected line
				for path := range tt.modified {
					if !tt.helper.Quiet {
						assert.Contains(t, output.String(), "Modified: "+path)
					}
				}
			}
		})
	}
}

func TestProcessOutput(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name           string
		helper         *Helper
		path           string
		processed      []string
		modified       map[string]string
		expectedOutput string
		expectError    bool
	}{
		{
			name: "QuietMode",
			helper: &Helper{
				Quiet: true,
				Cmd:   &cobra.Command{},
			},
			path:           "test/path",
			processed:      []string{"file1.txt", "file2.txt"},
			modified:       map[string]string{"file1.txt": "new content"},
			expectedOutput: "",
			expectError:    false,
		},
		{
			name: "DryRunMode",
			helper: &Helper{
				Quiet:  false,
				DryRun: true,
				Cmd:    &cobra.Command{},
			},
			path:           "test/path",
			processed:      []string{"file1.txt"},
			modified:       map[string]string{"file1.txt": "new content"},
			expectedOutput: "Processed: file1.txt\nModified: file1.txt\nnew content",
			expectError:    false,
		},
		{
			name: "ErrorOpeningFile",
			helper: &Helper{
				Quiet: false,
				Cmd:   &cobra.Command{},
			},
			path:           "invalid/path",
			modified:       map[string]string{"invalid/path": "new content"},
			expectedOutput: "",
			expectError:    true,
		},
	}

	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			// Set up command output
			var output strings.Builder
			tt.helper.Cmd.SetOut(&output)
			tt.helper.Cmd.SetErr(&output)

			// Create in-memory filesystem and add files
			fs := memfs.New()
			for path, content := range tt.modified {
				dir := filepath.Join(tt.path, filepath.Dir(path))
				assert.NoError(t, fs.MkdirAll(dir, 0755))
				file, err := fs.Create(filepath.Join(tt.path, path))
				if err == nil {
					_, _ = file.Write([]byte(content))
					assert.NoError(t, file.Close())
				}
			}

			// Process the output using the in-memory filesystem
			err := tt.helper.ProcessOutput(tt.path, tt.processed, tt.modified)
			if tt.expectError {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				assert.Contains(t, output.String(), tt.expectedOutput)
			}
		})
	}
}

func TestIsPath(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name     string
		setup    func(fs billy.Filesystem)
		path     string
		expected bool
	}{
		{
			name: "ExistingFile",
			setup: func(fs billy.Filesystem) {
				file, _ := fs.Create("testfile.txt")
				assert.NoError(t, file.Close())
			},
			path:     "testfile.txt",
			expected: true,
		},
		{
			name:     "NonExistentFile",
			setup:    func(_ billy.Filesystem) {},
			path:     "nonexistent.txt",
			expected: false,
		},
		{
			name: "ExistingDirectory",
			setup: func(fs billy.Filesystem) {
				assert.NoError(t, fs.MkdirAll("testdir", 0755))
			},
			path:     "testdir",
			expected: true,
		},
		{
			name:     "NonExistentDirectory",
			setup:    func(_ billy.Filesystem) {},
			path:     "nonexistentdir",
			expected: false,
		},
	}

	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			// Use in-memory filesystem for testing
			fs := memfs.New()
			tt.setup(fs)

			// Check if the path exists in the in-memory filesystem
			_, err := fs.Stat(tt.path)
			result := err == nil

			assert.Equal(t, tt.expected, result)
		})
	}
}
07070100000019000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000002000000000frizbee-0.1.7/internal/traverse0707010000001A000081A400000000000000000000000167E26F5900001229000000000000000000000000000000000000002C00000000frizbee-0.1.7/internal/traverse/traverse.go//
// Copyright 2024 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package traverse provides utilities to traverse directories.
package traverse

import (
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"strings"

	"github.com/go-git/go-billy/v5"
)

// GhwFunc is a function that gets called with each file in a GitHub Actions workflow
// directory. It receives the path to the file.
type GhwFunc func(path string) error

// FuncTraverse is a function that gets called with each file in a directory.
type FuncTraverse func(path string, info fs.FileInfo) error

// YamlDockerfiles traverses all yaml/yml in the given directory
// and calls the given function with each workflow.
func YamlDockerfiles(bfs billy.Filesystem, base string, fun GhwFunc) error {
	return Traverse(bfs, base, func(path string, info fs.FileInfo) error {
		if !isYAMLOrDockerfile(info) {
			return nil
		}

		if err := fun(path); err != nil {
			return fmt.Errorf("failed to process file %s: %w", path, err)
		}

		return nil
	})
}

// Traverse traverses the given directory and calls the given function with each file.
func Traverse(bfs billy.Filesystem, base string, fun FuncTraverse) error {
	return Walk(bfs, base, func(path string, info fs.FileInfo, err error) error {
		if err != nil {
			return nil
		}

		return fun(path, info)
	})
}

// isYAMLOrDockerfile returns true if the given file is a YAML or Dockerfile.
func isYAMLOrDockerfile(info fs.FileInfo) bool {
	// Skip if not a file
	if info.IsDir() {
		return false
	}

	// Filter out files that are not yml, yaml or dockerfiles
	if strings.HasSuffix(info.Name(), ".yml") || strings.HasSuffix(info.Name(), ".yaml") ||
		strings.Contains(strings.ToLower(info.Name()), "dockerfile") {
		return true
	}

	return false
}

// walk recursively descends path, calling walkFn
// adapted from https://golang.org/src/path/filepath/path.go
func walk(bfs billy.Filesystem, path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
	if !info.IsDir() {
		return walkFn(path, info, nil)
	}

	names, err := readDirNames(bfs, path)
	err1 := walkFn(path, info, err)
	// If err != nil, walk can't walk into this directory.
	// err1 != nil means walkFn want walk to skip this directory or stop walking.
	// Therefore, if one of err and err1 isn't nil, walk will return.
	if err != nil || err1 != nil {
		// The caller's behavior is controlled by the return value, which is decided
		// by walkFn. walkFn may ignore err and return nil.
		// If walkFn returns SkipDir, it will be handled by the caller.
		// So walk should return whatever walkFn returns.
		return err1
	}

	for _, name := range names {
		filename := filepath.Join(path, name)
		fileInfo, err := bfs.Lstat(filename)
		if err != nil {
			if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
				return err
			}
		} else {
			err = walk(bfs, filename, fileInfo, walkFn)
			if err != nil {
				if !fileInfo.IsDir() || err != filepath.SkipDir {
					return err
				}
			}
		}
	}
	return nil
}

// Walk walks the file tree rooted at root, calling fn for each file or
// directory in the tree, including root. All errors that arise visiting files
// and directories are filtered by fn: see the WalkFunc documentation for
// details.
//
// The files are walked in lexical order, which makes the output deterministic
// but requires Walk to read an entire directory into memory before proceeding
// to walk that directory. Walk does not follow symbolic links.
//
// Function adapted from https://github.com/golang/go/blob/3b770f2ccb1fa6fecc22ea822a19447b10b70c5c/src/path/filepath/path.go#L500
func Walk(bfs billy.Filesystem, root string, walkFn filepath.WalkFunc) error {
	info, err := bfs.Lstat(root)
	if err != nil {
		err = walkFn(root, nil, err)
	} else {
		err = walk(bfs, root, info, walkFn)
	}

	if err == filepath.SkipDir {
		return nil
	}

	return err
}

func readDirNames(bfs billy.Filesystem, dir string) ([]string, error) {
	files, err := bfs.ReadDir(dir)
	if err != nil {
		return nil, err
	}

	var names []string
	for _, file := range files {
		names = append(names, file.Name())
	}

	return names, nil
}
0707010000001B000081A400000000000000000000000167E26F59000014D2000000000000000000000000000000000000003100000000frizbee-0.1.7/internal/traverse/traverse_test.gopackage traverse

import (
	"errors"
	"os"
	"testing"
	"time"

	"github.com/go-git/go-billy/v5/memfs"
	"github.com/stretchr/testify/assert"
)

func TestYamlDockerfiles(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name        string
		fsContent   map[string]string
		baseDir     string
		expected    []string
		expectError bool
	}{
		{
			name: "NoYAMLOrDockerfile",
			fsContent: map[string]string{
				"base/file.txt": "content",
			},
			baseDir:     "base",
			expected:    []string{},
			expectError: false,
		},
		{
			name: "WithYAMLFiles",
			fsContent: map[string]string{
				"base/file.yml":         "content",
				"base/file.yaml":        "content",
				"base/not_included.txt": "content",
			},
			baseDir: "base",
			expected: []string{
				"base/file.yml",
				"base/file.yaml",
			},
			expectError: false,
		},
		{
			name: "WithDockerfiles",
			fsContent: map[string]string{
				"base/Dockerfile":        "content",
				"base/nested/dockerfile": "content",
				"base/not_included.txt":  "content",
			},
			baseDir: "base",
			expected: []string{
				"base/Dockerfile",
				"base/nested/dockerfile",
			},
			expectError: false,
		},
		{
			name: "MixedFiles",
			fsContent: map[string]string{
				"base/file.yml":          "content",
				"base/Dockerfile":        "content",
				"base/nested/file.yaml":  "content",
				"base/nested/dockerfile": "content",
				"base/not_included.txt":  "content",
			},
			baseDir: "base",
			expected: []string{
				"base/file.yml",
				"base/Dockerfile",
				"base/nested/file.yaml",
				"base/nested/dockerfile",
			},
			expectError: false,
		},
		{
			name: "ErrorInProcessingFile",
			fsContent: map[string]string{
				"base/file.yml": "content",
			},
			baseDir:     "base",
			expectError: true,
		},
	}

	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			fs := memfs.New()
			for name, content := range tt.fsContent {
				f, _ := fs.Create(name)
				_, _ = f.Write([]byte(content))
				assert.NoError(t, f.Close())
			}

			var processedFiles []string
			err := YamlDockerfiles(fs, tt.baseDir, func(path string) error {
				if tt.expectError {
					return errors.New("error in processing file")
				}
				processedFiles = append(processedFiles, path)
				return nil
			})

			if tt.expectError {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				assert.ElementsMatch(t, tt.expected, processedFiles)
			}
		})
	}
}

func TestTraverse(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name        string
		fsContent   map[string]string
		baseDir     string
		expected    []string
		expectError bool
	}{
		{
			name: "TraverseFiles",
			fsContent: map[string]string{
				"base/file1.txt":   "content",
				"base/file2.txt":   "content",
				"base/nested/file": "content",
			},
			baseDir: "base",
			expected: []string{
				"base",
				"base/file1.txt",
				"base/file2.txt",
				"base/nested",
				"base/nested/file",
			},
			expectError: false,
		},
		{
			name: "TraverseWithError",
			fsContent: map[string]string{
				"base/file.txt": "content",
			},
			baseDir:     "base",
			expectError: true,
		},
	}

	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			fs := memfs.New()
			for name, content := range tt.fsContent {
				f, _ := fs.Create(name)
				_, _ = f.Write([]byte(content))
				assert.NoError(t, f.Close())
			}

			var processedFiles []string
			err := Traverse(fs, tt.baseDir, func(path string, _ os.FileInfo) error {
				if tt.expectError {
					return errors.New("error in traversing file")
				}
				processedFiles = append(processedFiles, path)
				return nil
			})

			if tt.expectError {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				assert.ElementsMatch(t, tt.expected, processedFiles)
			}
		})
	}
}

func TestIsYAMLOrDockerfile(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name     string
		fileName string
		isDir    bool
		expected bool
	}{
		{
			name:     "YAMLFile",
			fileName: "config.yaml",
			isDir:    false,
			expected: true,
		},
		{
			name:     "YMLFile",
			fileName: "config.yml",
			isDir:    false,
			expected: true,
		},
		{
			name:     "Dockerfile",
			fileName: "Dockerfile",
			isDir:    false,
			expected: true,
		},
		{
			name:     "dockerfile",
			fileName: "dockerfile",
			isDir:    false,
			expected: true,
		},
		{
			name:     "NonYAMLOrDockerfile",
			fileName: "config.txt",
			isDir:    false,
			expected: false,
		},
		{
			name:     "Directory",
			fileName: "config",
			isDir:    true,
			expected: false,
		},
	}

	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			info := &fileInfoMock{
				name: tt.fileName,
				dir:  tt.isDir,
			}

			result := isYAMLOrDockerfile(info)
			assert.Equal(t, tt.expected, result)
		})
	}
}

// fileInfoMock is a mock implementation of os.FileInfo for testing.
type fileInfoMock struct {
	name string
	dir  bool
}

func (f *fileInfoMock) Name() string       { return f.name }
func (_ *fileInfoMock) Size() int64        { return 0 }
func (_ *fileInfoMock) Mode() os.FileMode  { return 0 }
func (_ *fileInfoMock) ModTime() time.Time { return time.Time{} }
func (f *fileInfoMock) IsDir() bool        { return f.dir }
func (_ *fileInfoMock) Sys() interface{}   { return nil }
0707010000001C000081A400000000000000000000000167E26F5900000089000000000000000000000000000000000000001600000000frizbee-0.1.7/main.go/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package main

import "github.com/stacklok/frizbee/cmd"

func main() {
	cmd.Execute()
}
0707010000001D000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001200000000frizbee-0.1.7/pkg0707010000001E000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001D00000000frizbee-0.1.7/pkg/interfaces0707010000001F000081A400000000000000000000000167E26F5900000747000000000000000000000000000000000000002B00000000frizbee-0.1.7/pkg/interfaces/interfaces.go//
// Copyright 2024 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package interfaces provides interfaces for the frizbee package.
package interfaces

import (
	"context"
	"errors"
	"net/http"

	"github.com/stacklok/frizbee/pkg/utils/config"
	"github.com/stacklok/frizbee/pkg/utils/store"
)

var (
	// ErrReferenceSkipped is returned when the reference is skipped.
	ErrReferenceSkipped = errors.New("reference skipped")
)

// EntityRef represents an action reference.
type EntityRef struct {
	Name   string `json:"name"`
	Ref    string `json:"ref"`
	Type   string `json:"type"`
	Tag    string `json:"tag"`
	Prefix string `json:"prefix"`
}

// Parser is an interface to replace references with digests
type Parser interface {
	SetCache(cache store.RefCacher)
	SetRegex(regex string)
	GetRegex() string
	Replace(ctx context.Context, matchedLine string, restIf REST, cfg config.Config) (*EntityRef, error)
	ConvertToEntityRef(reference string) (*EntityRef, error)
}

// The REST interface allows to wrap clients to talk to remotes
// When talking to GitHub, wrap a github client to provide this interface
type REST interface {
	// NewRequest creates an HTTP request.
	NewRequest(method, url string, body any) (*http.Request, error)
	// Do executes an HTTP request.
	Do(ctx context.Context, req *http.Request) (*http.Response, error)
}
07070100000020000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001B00000000frizbee-0.1.7/pkg/replacer07070100000021000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000002300000000frizbee-0.1.7/pkg/replacer/actions07070100000022000081A400000000000000000000000167E26F5900002BD1000000000000000000000000000000000000002E00000000frizbee-0.1.7/pkg/replacer/actions/actions.go//
// Copyright 2024 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package actions provides utilities to work with GitHub Actions.
package actions

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"slices"
	"strings"

	"github.com/google/go-github/v66/github"

	"github.com/stacklok/frizbee/pkg/interfaces"
	"github.com/stacklok/frizbee/pkg/replacer/image"
	"github.com/stacklok/frizbee/pkg/utils/config"
	"github.com/stacklok/frizbee/pkg/utils/store"
)

const (
	prefixUses   = "uses: "
	prefixDocker = "docker://"
	// GitHubActionsRegex is regular expression pattern to match GitHub Actions usage
	GitHubActionsRegex = `uses:\s*[^\s]+/[^\s]+@[^\s]+|uses:\s*docker://[^\s]+:[^\s]+`
	// ReferenceType is the type of the reference
	ReferenceType = "action"
)

var (
	// ErrInvalidAction is returned when parsing the action fails.
	ErrInvalidAction = errors.New("invalid action")
	// ErrInvalidActionReference is returned when parsing the action reference fails.
	ErrInvalidActionReference = errors.New("action reference is not a tag nor branch")
)

// Parser is a struct to replace action references with digests
type Parser struct {
	regex string
	cache store.RefCacher
}

// New creates a new Parser
func New() *Parser {
	return &Parser{
		regex: GitHubActionsRegex,
		cache: store.NewRefCacher(),
	}
}

// SetCache returns the regular expression pattern to match GitHub Actions usage
func (p *Parser) SetCache(cache store.RefCacher) {
	p.cache = cache
}

// SetRegex returns the regular expression pattern to match GitHub Actions usage
func (p *Parser) SetRegex(regex string) {
	p.regex = regex
}

// GetRegex returns the regular expression pattern to match GitHub Actions usage
func (p *Parser) GetRegex() string {
	return p.regex
}

// Replace replaces the action reference with the digest
func (p *Parser) Replace(
	ctx context.Context,
	matchedLine string,
	restIf interfaces.REST,
	cfg config.Config,
) (*interfaces.EntityRef, error) {
	var err error
	var actionRef *interfaces.EntityRef
	hasUsesPrefix := false

	// Trim the uses prefix
	if strings.HasPrefix(matchedLine, prefixUses) {
		matchedLine = strings.TrimPrefix(matchedLine, prefixUses)
		hasUsesPrefix = true
	}
	// Determine if the action reference has a docker prefix
	if strings.HasPrefix(matchedLine, prefixDocker) {
		actionRef, err = p.replaceDocker(ctx, matchedLine, restIf, cfg)
	} else {
		actionRef, err = p.replaceAction(ctx, matchedLine, restIf, cfg)
	}
	if err != nil {
		return nil, err
	}

	// Add back the uses prefix
	if hasUsesPrefix {
		actionRef.Prefix = fmt.Sprintf("%s%s", prefixUses, actionRef.Prefix)
	}

	// Return the new action reference
	return actionRef, nil
}

func (p *Parser) replaceAction(
	ctx context.Context,
	matchedLine string,
	restIf interfaces.REST,
	cfg config.Config,
) (*interfaces.EntityRef, error) {

	// If the value is a local path or should be excluded, skip it
	if isLocal(matchedLine) || shouldExclude(&cfg.GHActions, matchedLine) {
		return nil, fmt.Errorf("%w: %s", interfaces.ErrReferenceSkipped, matchedLine)
	}

	// Parse the action reference
	act, ref, err := ParseActionReference(matchedLine)
	if err != nil {
		return nil, fmt.Errorf("failed to parse action reference '%s': %w", matchedLine, err)
	}

	// Check if the parsed reference should be excluded
	if shouldExclude(&cfg.GHActions, act) {
		return nil, fmt.Errorf("%w: %s", interfaces.ErrReferenceSkipped, matchedLine)
	}
	var sum string

	// Check if we have a cache
	if p.cache != nil {
		// Check if we have a cached value
		if val, ok := p.cache.Load(matchedLine); ok {
			sum = val
		} else {
			// Get the checksum for the action reference
			sum, err = GetChecksum(ctx, cfg.GHActions, restIf, act, ref)
			if err != nil {
				return nil, fmt.Errorf("failed to get checksum for action '%s': %w", matchedLine, err)
			}
			// Store the checksum in the cache
			p.cache.Store(matchedLine, sum)
		}
	} else {
		// Get the checksum for the action reference
		sum, err = GetChecksum(ctx, cfg.GHActions, restIf, act, ref)
		if err != nil {
			return nil, fmt.Errorf("failed to get checksum for action '%s': %w", matchedLine, err)
		}
	}

	// Compare the digest with the reference and return the original reference if they already match
	if ref == sum {
		return nil, fmt.Errorf("image already referenced by digest: %s %w", matchedLine, interfaces.ErrReferenceSkipped)
	}

	return &interfaces.EntityRef{
		Name: act,
		Ref:  sum,
		Type: ReferenceType,
		Tag:  ref,
	}, nil
}

func (p *Parser) replaceDocker(
	ctx context.Context,
	matchedLine string,
	_ interfaces.REST,
	cfg config.Config,
) (*interfaces.EntityRef, error) {
	// Trim the docker prefix
	trimmedRef := strings.TrimPrefix(matchedLine, prefixDocker)

	// If the value is a local path or should be excluded, skip it
	if isLocal(trimmedRef) || shouldExclude(&cfg.GHActions, trimmedRef) {
		return nil, fmt.Errorf("%w: %s", interfaces.ErrReferenceSkipped, matchedLine)
	}

	// Get the digest of the docker:// image reference
	actionRef, err := image.GetImageDigestFromRef(ctx, trimmedRef, cfg.Platform, p.cache)
	if err != nil {
		return nil, err
	}

	// Check if the parsed reference should be excluded
	if shouldExclude(&cfg.GHActions, actionRef.Name) {
		return nil, fmt.Errorf("%w: %s", interfaces.ErrReferenceSkipped, matchedLine)
	}

	// Add back the docker prefix
	if strings.HasPrefix(matchedLine, prefixDocker) {
		actionRef.Prefix = fmt.Sprintf("%s%s", prefixDocker, actionRef.Prefix)
	}

	return actionRef, nil
}

// ConvertToEntityRef converts an action reference to an EntityRef
func (_ *Parser) ConvertToEntityRef(reference string) (*interfaces.EntityRef, error) {
	reference = strings.TrimPrefix(reference, prefixUses)
	refType := ReferenceType
	separator := "@"
	// Update the separator in case this is a docker reference with a digest
	if strings.Contains(reference, prefixDocker) {
		reference = strings.TrimPrefix(reference, prefixDocker)
		if !strings.Contains(reference, separator) && strings.Contains(reference, ":") {
			separator = ":"
		}
		refType = image.ReferenceType
	}
	frags := strings.Split(reference, separator)
	if len(frags) != 2 {
		return nil, fmt.Errorf("invalid action reference: %s", reference)
	}

	return &interfaces.EntityRef{
		Name: frags[0],
		Ref:  frags[1],
		Type: refType,
	}, nil
}

// isLocal returns true if the input is a local path.
func isLocal(input string) bool {
	return strings.HasPrefix(input, "./") || strings.HasPrefix(input, "../")
}

func shouldExclude(cfg *config.GHActions, input string) bool {
	for _, e := range cfg.Exclude {
		if e == input {
			return true
		}
	}
	return false
}

// ParseActionReference parses an action reference into action and reference.
func ParseActionReference(input string) (action string, reference string, err error) {
	frags := strings.Split(input, "@")
	if len(frags) != 2 {
		return "", "", fmt.Errorf("invalid action reference: %s", input)
	}

	return frags[0], frags[1], nil
}

// GetChecksum returns the checksum for a given action and tag.
func GetChecksum(ctx context.Context, cfg config.GHActions, restIf interfaces.REST, action, ref string) (string, error) {
	owner, repo, err := parseActionFragments(action)
	if err != nil {
		return "", err
	}

	// Check if we're using a checksum
	if isChecksum(ref) {
		return ref, nil
	}

	res, err := getCheckSumForTag(ctx, restIf, owner, repo, ref)
	if err != nil {
		return "", fmt.Errorf("failed to get checksum for tag: %w", err)
	} else if res != "" {
		return res, nil
	}

	// check branch
	if excludeBranch(cfg.Filter.ExcludeBranches, ref) {
		// if a branch is excluded, we won't know if it's a valid reference
		// but that's OK - we just won't touch that reference
		return "", fmt.Errorf("%w: %s", interfaces.ErrReferenceSkipped, ref)
	}

	res, err = getCheckSumForBranch(ctx, restIf, owner, repo, ref)
	if err != nil {
		return "", fmt.Errorf("failed to get checksum for branch: %w", err)
	} else if res != "" {
		return res, nil
	}

	return "", ErrInvalidActionReference
}

func parseActionFragments(action string) (owner string, repo string, err error) {
	frags := strings.Split(action, "/")

	// if we have more than 2 fragments, we're probably dealing with
	// sub-actions, so we take the first two fragments as the owner and repo
	if len(frags) < 2 {
		return "", "", fmt.Errorf("%w: '%s' reference is incorrect", ErrInvalidAction, action)
	}

	return frags[0], frags[1], nil
}

// isChecksum returns true if the input is a checksum.
func isChecksum(ref string) bool {
	return len(ref) == 40
}

func getCheckSumForTag(ctx context.Context, restIf interfaces.REST, owner, repo, tag string) (string, error) {
	path, err := url.JoinPath("repos", owner, repo, "git", "refs", "tags", tag)
	if err != nil {
		return "", fmt.Errorf("failed to join path: %w", err)
	}

	sha, otype, err := doGetReference(ctx, restIf, path)
	if err != nil {
		return "", err
	}

	if otype == "commit" {
		return sha, nil
	}

	// assume otype == "tag"
	path, err = url.JoinPath("repos", owner, repo, "git", "tags", sha)
	if err != nil {
		return "", fmt.Errorf("failed to join path: %w", err)
	}

	sha, _, err = doGetReference(ctx, restIf, path)
	return sha, err
}

func getCheckSumForBranch(ctx context.Context, restIf interfaces.REST, owner, repo, branch string) (string, error) {
	path, err := url.JoinPath("repos", owner, repo, "git", "refs", "heads", branch)
	if err != nil {
		return "", fmt.Errorf("failed to join path: %w", err)
	}

	sha, _, err := doGetReference(ctx, restIf, path)
	return sha, err
}

func excludeBranch(excludes []string, branch string) bool {
	if len(excludes) == 0 {
		return false
	}
	if slices.Contains(excludes, "*") {
		return true
	}

	return slices.Contains(excludes, branch)
}

func doGetReference(ctx context.Context, restIf interfaces.REST, path string) (string, string, error) {
	req, err := restIf.NewRequest(http.MethodGet, path, nil)
	if err != nil {
		return "", "", fmt.Errorf("cannot create REST request: %w", err)
	}

	resp, err := restIf.Do(ctx, req)

	if resp != nil {
		defer func() {
			_ = resp.Body.Close()
		}()
	}

	if err != nil && resp.StatusCode != http.StatusNotFound {
		return "", "", fmt.Errorf("failed to do API request: %w", err)
	} else if resp.StatusCode == http.StatusNotFound {
		// No error, but no tag found
		return "", "", nil
	}

	var t github.Reference
	err = json.NewDecoder(resp.Body).Decode(&t)
	if err != nil && strings.Contains(err.Error(), "cannot unmarshal array into Go value of type") {
		// This is a branch, not a tag
		return "", "", nil
	} else if err != nil {
		return "", "", fmt.Errorf("canont decode response: %w", err)
	}

	return t.GetObject().GetSHA(), t.GetObject().GetType(), nil
}
07070100000023000081A400000000000000000000000167E26F5900001FCE000000000000000000000000000000000000003300000000frizbee-0.1.7/pkg/replacer/actions/actions_test.gopackage actions

import (
	"context"
	"os"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/stacklok/frizbee/pkg/utils/config"
	"github.com/stacklok/frizbee/pkg/utils/ghrest"
	"github.com/stacklok/frizbee/pkg/utils/store"
)

func TestNewParser(t *testing.T) {
	t.Parallel()

	parser := New()
	require.NotNil(t, parser, "Parser should not be nil")
	require.Equal(t, GitHubActionsRegex, parser.regex, "Default regex should be GitHubActionsRegex")
	require.NotNil(t, parser.cache, "Cache should be initialized")
}

func TestSetCache(t *testing.T) {
	t.Parallel()

	parser := New()
	cache := store.NewRefCacher()
	parser.SetCache(cache)
	require.Equal(t, cache, parser.cache, "Cache should be set correctly")
}

func TestSetAndGetRegex(t *testing.T) {
	t.Parallel()

	parser := New()
	tests := []struct {
		name     string
		newRegex string
	}{
		{
			name:     "Set and get new regex",
			newRegex: `new-regex`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			parser.SetRegex(tt.newRegex)
			require.Equal(t, tt.newRegex, parser.GetRegex(), "Regex should be set and retrieved correctly")
		})
	}
}

func TestReplaceLocalPath(t *testing.T) {
	t.Parallel()

	parser := New()
	ctx := context.Background()
	cfg := config.Config{}
	restIf := &ghrest.Client{}

	tests := []struct {
		name        string
		matchedLine string
	}{
		{
			name:        "Replace local path",
			matchedLine: "./local/path",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			_, err := parser.Replace(ctx, tt.matchedLine, restIf, cfg)
			require.Error(t, err, "Should return error for local path")
			require.Contains(t, err.Error(), "reference skipped", "Error should indicate reference skipped")
		})
	}
}

func TestReplaceExcludedPath(t *testing.T) {
	t.Parallel()

	parser := New()
	ctx := context.Background()
	cfg := config.Config{GHActions: config.GHActions{Filter: config.Filter{Exclude: []string{"actions/checkout"}}}}
	restIf := &ghrest.Client{}

	tests := []struct {
		name        string
		matchedLine string
	}{
		{
			name:        "Replace excluded path",
			matchedLine: "uses: actions/checkout@v2",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			_, err := parser.Replace(ctx, tt.matchedLine, restIf, cfg)
			require.Error(t, err, "Should return error for excluded path")
			require.Contains(t, err.Error(), "reference skipped", "Error should indicate reference skipped")
		})
	}
}

func TestConvertToEntityRef(t *testing.T) {
	t.Parallel()

	parser := New()

	tests := []struct {
		name      string
		reference string
		wantErr   bool
	}{
		{"Valid action reference", "uses: actions/checkout@v2", false},
		{"Valid docker reference", "docker://mydocker/image:tag", false},
		{"Invalid reference format", "invalid-reference", true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			ref, err := parser.ConvertToEntityRef(tt.reference)
			if tt.wantErr {
				require.Error(t, err, "Expected error but got none")
			} else {
				require.NoError(t, err, "Expected no error but got %v", err)
				require.NotNil(t, ref, "EntityRef should not be nil")
			}
		})
	}
}

func TestIsLocal(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name  string
		input string
		want  bool
	}{
		{"Local path with ./", "./local/path", true},
		{"Local path with ../", "../local/path", true},
		{"Non-local path", "non/local/path", false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			require.Equal(t, tt.want, isLocal(tt.input), "IsLocal should return correct value")
		})
	}
}

func TestShouldExclude(t *testing.T) {
	t.Parallel()

	cfg := &config.GHActions{Filter: config.Filter{Exclude: []string{"actions/checkout", "actions/setup"}}}

	tests := []struct {
		name  string
		input string
		want  bool
	}{
		{"Excluded path", "actions/checkout", true},
		{"Non-excluded path", "actions/unknown", false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			require.Equal(t, tt.want, shouldExclude(cfg, tt.input), "ShouldExclude should return correct value")
		})
	}
}

func TestParseActionReference(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name       string
		input      string
		wantAction string
		wantRef    string
		wantErr    bool
	}{
		{"Valid action reference", "actions/checkout@v2", "actions/checkout", "v2", false},
		{"Invalid reference format", "invalid-reference", "", "", true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			action, ref, err := ParseActionReference(tt.input)
			if tt.wantErr {
				require.Error(t, err, "Expected error but got none")
			} else {
				require.NoError(t, err, "Expected no error but got %v", err)
				require.Equal(t, tt.wantAction, action, "Action should be parsed correctly")
				require.Equal(t, tt.wantRef, ref, "Reference should be parsed correctly")
			}
		})
	}
}

func TestGetChecksum(t *testing.T) {
	t.Parallel()

	tok := os.Getenv("GITHUB_TOKEN")
	ctx := context.Background()
	ghcli := ghrest.NewClient(tok)

	tests := []struct {
		name    string
		args    struct{ action, ref string }
		want    string
		wantErr bool
	}{
		{
			name:    "actions/checkout with v4.1.1",
			args:    struct{ action, ref string }{action: "actions/checkout", ref: "v4.1.1"},
			want:    "b4ffde65f46336ab88eb53be808477a3936bae11",
			wantErr: false,
		},
		{
			name:    "actions/checkout with v3.6.0",
			args:    struct{ action, ref string }{action: "actions/checkout", ref: "v3.6.0"},
			want:    "f43a0e5ff2bd294095638e18286ca9a3d1956744",
			wantErr: false,
		},
		{
			name:    "actions/checkout with checksum returns checksum",
			args:    struct{ action, ref string }{action: "actions/checkout", ref: "1d96c772d19495a3b5c517cd2bc0cb401ea0529f"},
			want:    "1d96c772d19495a3b5c517cd2bc0cb401ea0529f",
			wantErr: false,
		},
		{
			name:    "aquasecurity/trivy-action with 0.14.0",
			args:    struct{ action, ref string }{action: "aquasecurity/trivy-action", ref: "0.14.0"},
			want:    "2b6a709cf9c4025c5438138008beaddbb02086f0",
			wantErr: false,
		},
		{
			name:    "aquasecurity/trivy-action with branch returns checksum",
			args:    struct{ action, ref string }{action: "aquasecurity/trivy-action", ref: "bump-trivy"},
			want:    "fb5e1b36be448e92ca98648c661bd7e9da1f1317",
			wantErr: false,
		},
		{
			name:    "actions/checkout with invalid tag returns error",
			args:    struct{ action, ref string }{action: "actions/checkout", ref: "v4.1.1.1"},
			want:    "",
			wantErr: true,
		},
		{
			name:    "actions/checkout with invalid action returns error",
			args:    struct{ action, ref string }{action: "invalid-action", ref: "v4.1.1"},
			want:    "",
			wantErr: true,
		},
		{
			name:    "actions/checkout with empty action returns error",
			args:    struct{ action, ref string }{action: "", ref: "v4.1.1"},
			want:    "",
			wantErr: true,
		},
		{
			name:    "actions/checkout with empty tag returns error",
			args:    struct{ action, ref string }{action: "actions/checkout", ref: ""},
			want:    "",
			wantErr: true,
		},
		{
			name:    "actions/setup-node with v1 is an array",
			args:    struct{ action, ref string }{action: "actions/setup-node", ref: "v1"},
			want:    "f1f314fca9dfce2769ece7d933488f076716723e",
			wantErr: false,
		},
		{
			name:    "anchore/sbom-action/download-syft with a sub-action works",
			args:    struct{ action, ref string }{action: "anchore/sbom-action/download-syft", ref: "v0.14.3"},
			want:    "78fc58e266e87a38d4194b2137a3d4e9bcaf7ca1",
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			got, err := GetChecksum(ctx, config.GHActions{}, ghcli, tt.args.action, tt.args.ref)
			if tt.wantErr {
				require.Error(t, err, "Wanted error, got none")
				require.Empty(t, got, "Wanted empty string, got %v", got)
				return
			}
			require.NoError(t, err, "Wanted no error, got %v", err)
			require.Equal(t, tt.want, got, "Wanted %v, got %v", tt.want, got)
		})
	}
}
07070100000024000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000002100000000frizbee-0.1.7/pkg/replacer/image07070100000025000081A400000000000000000000000167E26F5900002043000000000000000000000000000000000000002A00000000frizbee-0.1.7/pkg/replacer/image/image.go//
// Copyright 2024 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package image provides utilities to work with container images.
package image

import (
	"context"
	"errors"
	"fmt"
	"slices"
	"strings"

	"github.com/google/go-containerregistry/pkg/authn"
	"github.com/google/go-containerregistry/pkg/name"
	v1 "github.com/google/go-containerregistry/pkg/v1"
	"github.com/google/go-containerregistry/pkg/v1/remote"
	dockerparser "github.com/moby/buildkit/frontend/dockerfile/parser"

	"github.com/stacklok/frizbee/internal/cli"
	"github.com/stacklok/frizbee/pkg/interfaces"
	"github.com/stacklok/frizbee/pkg/utils/config"
	"github.com/stacklok/frizbee/pkg/utils/store"
)

const (
	// ContainerImageRegex is regular expression pattern to match container image usage in YAML
	// nolint:lll
	ContainerImageRegex = `image\s*:\s*["']?([^\s"']+/[^\s"']+|[^\s"']+)(:[^\s"']+)?(@[^\s"']+)?["']?|FROM\s+(--platform=[^\s]+[^\s]*\s+)?([^\s]+(/[^\s]+)?(:[^\s]+)?(@[^\s]+)?)`
	prefixFROM          = "FROM "
	prefixImage         = "image: "
	// ReferenceType is the type of the reference
	ReferenceType = "container"
)

// Parser is a struct to replace container image references with digests
type Parser struct {
	regex string
	cache store.RefCacher
}

type unresolvedImage struct {
	imageRef string
	flags    []string
}

// New creates a new Parser
func New() *Parser {
	return &Parser{
		regex: ContainerImageRegex,
		cache: store.NewRefCacher(),
	}
}

// SetCache sets the cache to store the image references
func (p *Parser) SetCache(cache store.RefCacher) {
	p.cache = cache
}

// SetRegex sets the regular expression pattern to match container image usage
func (p *Parser) SetRegex(regex string) {
	p.regex = regex
}

// GetRegex returns the regular expression pattern to match container image usage
func (p *Parser) GetRegex() string {
	return p.regex
}

// Replace replaces the container image reference with the digest
func (p *Parser) Replace(
	ctx context.Context,
	matchedLine string,
	_ interfaces.REST,
	cfg config.Config,
) (*interfaces.EntityRef, error) {
	var imageRef string
	var extraArgs string

	// Trim the prefix
	hasFROMPrefix := false
	hasImagePrefix := false
	// Check if the image reference has the FROM prefix, i.e. Dockerfile
	if strings.HasPrefix(matchedLine, prefixFROM) {
		parsedFrom, err := getRefFromDockerfileFROM(matchedLine)
		if err != nil {
			return nil, err
		}

		// Check if the image reference should be excluded, i.e. scratch
		if shouldSkipImageRef(&cfg, parsedFrom.imageRef) {
			return nil, fmt.Errorf("image reference %s should be excluded - %w", matchedLine, interfaces.ErrReferenceSkipped)
		}

		imageRef = parsedFrom.imageRef
		extraArgs = strings.Join(parsedFrom.flags, " ")
		if extraArgs != "" {
			extraArgs += " "
		}

		hasFROMPrefix = true
	} else if strings.HasPrefix(matchedLine, prefixImage) {
		// Check if the image reference has the image prefix, i.e. Kubernetes or Docker Compose YAML
		imageRef = strings.TrimPrefix(matchedLine, prefixImage)
		// Check if the image reference should be excluded, i.e. scratch
		if shouldSkipImageRef(&cfg, imageRef) {
			return nil, fmt.Errorf("image reference %s should be excluded - %w", matchedLine, interfaces.ErrReferenceSkipped)
		}
		hasImagePrefix = true
	} else {
		imageRef = matchedLine
	}

	// Get the digest of the image reference
	imageRefWithDigest, err := GetImageDigestFromRef(ctx, imageRef, cfg.Platform, p.cache)
	if err != nil {
		return nil, err
	}

	// Add the prefix back
	if hasFROMPrefix {
		imageRefWithDigest.Prefix = fmt.Sprintf("%s%s%s", prefixFROM, extraArgs, imageRefWithDigest.Prefix)
	} else if hasImagePrefix {
		imageRefWithDigest.Prefix = fmt.Sprintf("%s%s", prefixImage, imageRefWithDigest.Prefix)
	}

	// Return the reference
	return imageRefWithDigest, nil
}

// ConvertToEntityRef converts a container image reference to an EntityRef
func (_ *Parser) ConvertToEntityRef(reference string) (*interfaces.EntityRef, error) {
	reference = strings.TrimPrefix(reference, prefixImage)
	reference = strings.TrimPrefix(reference, prefixFROM)
	var sep string
	var frags []string
	if strings.Contains(reference, "@") {
		sep = "@"
	} else if strings.Contains(reference, ":") {
		sep = ":"
	}

	if sep != "" {
		frags = strings.Split(reference, sep)
		if len(frags) != 2 {
			return nil, fmt.Errorf("invalid container reference: %s", reference)
		}
	} else {
		frags = []string{reference, "latest"}
	}

	return &interfaces.EntityRef{
		Name: frags[0],
		Ref:  frags[1],
		Type: ReferenceType,
	}, nil
}

// GetImageDigestFromRef returns the digest of a container image reference
// from a name.Reference.
func GetImageDigestFromRef(ctx context.Context, imageRef, platform string, cache store.RefCacher) (*interfaces.EntityRef, error) {
	// Parse the image reference
	ref, err := name.ParseReference(imageRef)
	if err != nil {
		return nil, err
	}
	opts := []remote.Option{
		remote.WithContext(ctx),
		remote.WithUserAgent(cli.UserAgent),
		remote.WithAuthFromKeychain(authn.DefaultKeychain),
	}

	// Set the platform if provided
	if platform != "" {
		platformSplit := strings.Split(platform, "/")
		if len(platformSplit) != 2 {
			return nil, errors.New("platform must be in the format os/arch")
		}
		opts = append(opts, remote.WithPlatform(v1.Platform{
			OS:           platformSplit[0],
			Architecture: platformSplit[1],
		}))
	}

	// Get the digest of the image reference
	var digest string

	if cache != nil {
		if d, ok := cache.Load(imageRef); ok {
			digest = d
		} else {
			desc, err := remote.Get(ref, opts...)
			if err != nil {
				return nil, err
			}
			digest = desc.Digest.String()
			cache.Store(imageRef, digest)
		}
	} else {
		desc, err := remote.Get(ref, opts...)
		if err != nil {
			return nil, err
		}
		digest = desc.Digest.String()
	}

	// Compare the digest with the reference and return the original reference if they already match
	if digest == ref.Identifier() {
		return nil, fmt.Errorf("image already referenced by digest: %s %w", imageRef, interfaces.ErrReferenceSkipped)
	}

	return &interfaces.EntityRef{
		Name: ref.Context().Name(),
		Ref:  digest,
		Type: ReferenceType,
		Tag:  ref.Identifier(),
	}, nil
}

func shouldSkipImageRef(cfg *config.Config, ref string) bool {
	// Parse the image reference
	nameRef, err := name.ParseReference(ref)
	if err != nil {
		// we wouldn't know how to resolve this reference, so let's skip
		return true
	}

	imageName := getImageNameFromRef(nameRef)
	if slices.Contains(cfg.Images.ImageFilter.ExcludeImages, imageName) {
		return true
	}

	tag := nameRef.Identifier()
	return slices.Contains(cfg.Images.ImageFilter.ExcludeTags, tag)
}

// TODO(jakub): this is a bit of a hack, but I didn't find a better way to get just the name
func getImageNameFromRef(nameRef name.Reference) string {
	fullRepositoryName := nameRef.Context().Name()
	parts := strings.Split(fullRepositoryName, "/")
	if len(parts) > 1 {
		return parts[len(parts)-1]
	}

	return ""
}
func getRefFromDockerfileFROM(line string) (unresolvedImage, error) {
	parseResult, err := dockerparser.Parse(strings.NewReader(line))
	if err != nil {
		return unresolvedImage{}, fmt.Errorf("failed to parse Dockerfile line: %w", err)
	}

	if len(parseResult.AST.Children) == 0 ||
		parseResult.AST.Children[0] == nil ||
		strings.ToUpper(parseResult.AST.Children[0].Value) != "FROM" {
		return unresolvedImage{}, errors.New("invalid Dockerfile line: the first parsed node is not FROM")
	}

	fromNode := parseResult.AST.Children[0]

	imgNode := parseResult.AST.Children[0].Next
	if imgNode == nil {
		return unresolvedImage{}, errors.New("invalid Dockerfile line: no image node found")
	}

	return unresolvedImage{
		imageRef: imgNode.Value,
		flags:    fromNode.Flags,
	}, nil
}
07070100000026000081A400000000000000000000000167E26F59000017C4000000000000000000000000000000000000002F00000000frizbee-0.1.7/pkg/replacer/image/image_test.gopackage image

import (
	"context"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/stacklok/frizbee/pkg/interfaces"
	"github.com/stacklok/frizbee/pkg/utils/config"
	"github.com/stacklok/frizbee/pkg/utils/store"
)

func TestNewParser(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name string
	}{
		{"New parser initialization"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			parser := New()
			require.NotNil(t, parser, "Parser should not be nil")
			require.Equal(t, ContainerImageRegex, parser.regex, "Default regex should be ContainerImageRegex")
			require.NotNil(t, parser.cache, "Cache should be initialized")
		})
	}
}

func TestSetCache(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name  string
		cache store.RefCacher
	}{
		{"Set cache for parser", store.NewRefCacher()},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			parser := New()
			parser.SetCache(tt.cache)
			require.Equal(t, tt.cache, parser.cache, "Cache should be set correctly")
		})
	}
}

func TestSetAndGetRegex(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name     string
		newRegex string
	}{
		{"Set and get new regex", `new-regex`},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			parser := New()
			parser.SetRegex(tt.newRegex)
			require.Equal(t, tt.newRegex, parser.GetRegex(), "Regex should be set and retrieved correctly")
		})
	}
}

func TestReplaceExcludedPath(t *testing.T) {
	t.Parallel()

	parser := New()
	ctx := context.Background()
	cfg := config.Config{
		Images: config.Images{
			ImageFilter: config.ImageFilter{
				ExcludeImages: []string{"scratch"},
				ExcludeTags:   []string{"latest"},
			},
		},
	}

	tests := []struct {
		name        string
		matchedLine string
		expected    error
	}{
		{
			"Do not replace scratch FROM image",
			"FROM scratch",
			interfaces.ErrReferenceSkipped,
		},
		{
			"Do not replace ubuntu:latest",
			"FROM ubuntu:latest",
			interfaces.ErrReferenceSkipped,
		},
		{
			"Do not replace ubuntu:latest with AS",
			"FROM ubuntu:latest AS builder",
			interfaces.ErrReferenceSkipped,
		},
		{
			"Do not replace ubuntu without a tag",
			"FROM ubuntu",
			interfaces.ErrReferenceSkipped,
		},
		{
			"Do not replace ubuntu without a tag with a stage",
			"FROM ubuntu AS builder",
			interfaces.ErrReferenceSkipped,
		},
		{
			"Replace ubuntu:22.04",
			"FROM ubuntu:22.04",
			nil,
		},
		{
			"Replace ubuntu:22.04 with AS",
			"FROM ubuntu:22.04 AS builder",
			nil,
		},
		{
			"Replace ubuntu:22.04 with AS",
			"FROM --platform=linux/amd64 ubuntu:22.04 AS builder",
			nil,
		},
		{
			"Replace with repo reference and tag",
			"FROM ghcr.io/stacklok/minder/helm/minder:0.20231123.829_ref.26ca90b",
			nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			_, err := parser.Replace(ctx, tt.matchedLine, nil, cfg)
			if tt.expected == nil {
				require.NoError(t, err, "Should not return error for excluded path")
			} else {
				require.Error(t, err, "Should return error for excluded path")
				require.ErrorIs(t, err, tt.expected, "Unexpected error")
			}
		})
	}
}

func TestConvertToEntityRef(t *testing.T) {
	t.Parallel()

	parser := New()

	tests := []struct {
		name      string
		reference string
		wantErr   bool
	}{
		{"Valid container reference with tag", "ghcr.io/stacklok/minder/helm/minder:0.20231123.829_ref.26ca90b", false},
		{"Valid container reference with digest", "ghcr.io/stacklok/minder/helm/minder@sha256:a29f8a8d28f0af7f70a4b3dd3e33c8c8cc5cf9e88e802e2700cf272a0b6140ec", false},
		{"Invalid reference format", "invalid:reference:format", true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			ref, err := parser.ConvertToEntityRef(tt.reference)
			if tt.wantErr {
				require.Error(t, err, "Expected error but got none")
			} else {
				require.NoError(t, err, "Expected no error but got %v", err)
				require.NotNil(t, ref, "EntityRef should not be nil")
			}
		})
	}
}

func TestGetImageDigestFromRef(t *testing.T) {
	t.Parallel()

	ctx := context.Background()

	tests := []struct {
		name    string
		refstr  string
		want    string
		wantErr bool
	}{
		{
			name:   "Valid image reference 1",
			refstr: "ghcr.io/stacklok/minder/helm/minder:0.20231123.829_ref.26ca90b",
			want:   "sha256:a29f8a8d28f0af7f70a4b3dd3e33c8c8cc5cf9e88e802e2700cf272a0b6140ec",
		},
		{
			name:   "Valid image reference 2",
			refstr: "devopsfaith/krakend:2.5.0",
			want:   "sha256:6a3c8e5e1a4948042bfb364ed6471e16b4a26d0afb6c3c01ebcb88b3fa551036",
		},
		{
			name:    "Invalid ref string",
			refstr:  "ghcr.io/stacklok/minder/helm/minder!",
			wantErr: true,
		},
		{
			name:    "Nonexistent container in nonexistent registry",
			refstr:  "beeeeer.io/ipa/toppling-goliath/king-sue:1.0.0",
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			got, err := GetImageDigestFromRef(ctx, tt.refstr, "", nil)
			if tt.wantErr {
				require.Error(t, err)
				require.Nil(t, got)
				return
			}

			require.NoError(t, err)
			require.Equal(t, tt.want, got.Ref)
		})
	}
}

func TestShouldSkipImage(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name string
		ref  string
		skip bool
	}{
		// skip cases
		{"Skip scratch", "scratch", true},
		{"Skip ubuntu without a tag", "ubuntu", true},
		{"Skip ubuntu:latest", "ubuntu:latest", true},
		// keep cases
		{"Do not skip ubuntu:22.04", "ubuntu:22.04", false},
		{"Do not skip with repo reference and tag", "myrepo/myimage:1.2.3", false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			config := &config.Config{
				Images: config.Images{
					ImageFilter: config.ImageFilter{
						ExcludeImages: []string{"scratch"},
						ExcludeTags:   []string{"latest"},
					},
				},
			}

			got := shouldSkipImageRef(config, tt.ref)
			require.Equal(t, tt.skip, got, "shouldSkipImageRef should return the correct exclusion status")
		})
	}
}
07070100000027000081A400000000000000000000000167E26F5900002846000000000000000000000000000000000000002700000000frizbee-0.1.7/pkg/replacer/replacer.go//
// Copyright 2024 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package replacer provide common replacer implementation
package replacer

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"sync"

	mapset "github.com/deckarep/golang-set/v2"
	"github.com/go-git/go-billy/v5"
	"github.com/go-git/go-billy/v5/osfs"
	"golang.org/x/sync/errgroup"

	"github.com/stacklok/frizbee/internal/traverse"
	"github.com/stacklok/frizbee/pkg/interfaces"
	"github.com/stacklok/frizbee/pkg/replacer/actions"
	"github.com/stacklok/frizbee/pkg/replacer/image"
	"github.com/stacklok/frizbee/pkg/utils/config"
	"github.com/stacklok/frizbee/pkg/utils/ghrest"
)

// ReplaceResult holds a slice of all processed files along with a map of their modified content
type ReplaceResult struct {
	Processed []string
	Modified  map[string]string
}

// ListResult holds the result of the list methods
type ListResult struct {
	Processed []string
	Entities  []interfaces.EntityRef
}

// Replacer is an object with methods to replace references with digests
type Replacer struct {
	parser interfaces.Parser
	rest   interfaces.REST
	cfg    config.Config
}

// NewGitHubActionsReplacer creates a new replacer for GitHub actions
func NewGitHubActionsReplacer(cfg *config.Config) *Replacer {
	cfg = config.MergeUserConfig(cfg)

	return &Replacer{
		cfg:    *cfg,
		parser: actions.New(),
		rest:   ghrest.NewClient(""),
	}
}

// NewContainerImagesReplacer creates a new replacer for container images
func NewContainerImagesReplacer(cfg *config.Config) *Replacer {
	cfg = config.MergeUserConfig(cfg)

	return &Replacer{
		cfg:    *cfg,
		parser: image.New(),
		rest:   ghrest.NewClient(""),
	}
}

// WithGitHubClientFromToken creates an authenticated GitHub client from a token
func (r *Replacer) WithGitHubClientFromToken(token string) *Replacer {
	client := ghrest.NewClient(token)
	r.rest = client
	return r
}

// WithGitHubClient sets the GitHub client to use
func (r *Replacer) WithGitHubClient(client interfaces.REST) *Replacer {
	r.rest = client
	return r
}

// WithUserRegex sets a user-provided regex for the parser
func (r *Replacer) WithUserRegex(regex string) *Replacer {
	if r.parser != nil && regex != "" {
		r.parser.SetRegex(regex)
	}
	return r
}

// WithCacheDisabled disables caching
func (r *Replacer) WithCacheDisabled() *Replacer {
	r.parser.SetCache(nil)
	return r
}

// ParseString parses and returns the referenced entity pinned by its digest
func (r *Replacer) ParseString(ctx context.Context, entityRef string) (*interfaces.EntityRef, error) {
	return r.parser.Replace(ctx, entityRef, r.rest, r.cfg)
}

// ParsePath parses and replaces all entity references in the provided directory
func (r *Replacer) ParsePath(ctx context.Context, dir string) (*ReplaceResult, error) {
	return parsePathInFS(ctx, r.parser, r.rest, r.cfg, osfs.New(filepath.Dir(dir), osfs.WithBoundOS()), filepath.Base(dir))
}

// ParsePathInFS parses and replaces all entity references in the provided file system
func (r *Replacer) ParsePathInFS(ctx context.Context, bfs billy.Filesystem, base string) (*ReplaceResult, error) {
	return parsePathInFS(ctx, r.parser, r.rest, r.cfg, bfs, base)
}

// ParseFile parses and replaces all entity references in the provided file
func (r *Replacer) ParseFile(ctx context.Context, f io.Reader) (bool, string, error) {
	return parseAndReplaceReferencesInFile(ctx, f, r.parser, r.rest, r.cfg)
}

// ListPath lists all entity references in the provided directory
func (r *Replacer) ListPath(dir string) (*ListResult, error) {
	return listReferencesInFS(r.parser, osfs.New(filepath.Dir(dir), osfs.WithBoundOS()), filepath.Base(dir))
}

// ListPathInFS lists all entity references in the provided file system
func (r *Replacer) ListPathInFS(bfs billy.Filesystem, base string) (*ListResult, error) {
	return listReferencesInFS(r.parser, bfs, base)
}

// ListInFile lists all entities in the provided file
func (r *Replacer) ListInFile(f io.Reader) (*ListResult, error) {
	found, err := listReferencesInFile(f, r.parser)
	if err != nil {
		return nil, err
	}
	res := &ListResult{}
	res.Entities = found.ToSlice()

	// Sort the slice
	sort.Slice(res.Entities, func(i, j int) bool {
		return res.Entities[i].Name < res.Entities[j].Name
	})

	// All good
	return res, nil
}

func parsePathInFS(
	ctx context.Context,
	parser interfaces.Parser,
	rest interfaces.REST,
	cfg config.Config,
	bfs billy.Filesystem,
	base string,
) (*ReplaceResult, error) {
	var eg errgroup.Group
	var mu sync.Mutex

	res := ReplaceResult{
		Processed: make([]string, 0),
		Modified:  make(map[string]string),
	}

	// Traverse all YAML/YML files in dir
	err := traverse.YamlDockerfiles(bfs, base, func(path string) error {
		eg.Go(func() error {
			file, err := bfs.Open(path)
			if err != nil {
				return fmt.Errorf("failed to open file %s: %w", path, err)
			}
			// nolint:errcheck // ignore error
			defer file.Close()

			// Parse the content of the file and update the matching references
			modified, updatedFile, err := parseAndReplaceReferencesInFile(ctx, file, parser, rest, cfg)
			if err != nil {
				return fmt.Errorf("failed to modify references in %s: %w", path, err)
			}

			mu.Lock()
			// Store the file name to the processed batch
			res.Processed = append(res.Processed, path)
			// Store the updated file content if it was modified
			if modified {
				res.Modified[path] = updatedFile
			}
			mu.Unlock()

			// All good
			return nil
		})
		return nil
	})
	if err != nil {
		return nil, err
	}

	if err := eg.Wait(); err != nil {
		return nil, err
	}

	// All good
	return &res, nil
}

func listReferencesInFS(parser interfaces.Parser, bfs billy.Filesystem, base string) (*ListResult, error) {
	var eg errgroup.Group
	var mu sync.Mutex

	res := ListResult{
		Processed: make([]string, 0),
		Entities:  make([]interfaces.EntityRef, 0),
	}

	found := mapset.NewSet[interfaces.EntityRef]()

	// Traverse all related files
	err := traverse.YamlDockerfiles(bfs, base, func(path string) error {
		eg.Go(func() error {
			file, err := bfs.Open(path)
			if err != nil {
				return fmt.Errorf("failed to open file %s: %w", path, err)
			}
			defer file.Close() // nolint:errcheck

			// Parse the content of the file and list the matching references
			foundRefs, err := listReferencesInFile(file, parser)
			if err != nil {
				return fmt.Errorf("failed to list references in %s: %w", path, err)
			}

			// Store the file name to the processed batch
			mu.Lock()
			res.Processed = append(res.Processed, path)
			found = found.Union(foundRefs)
			mu.Unlock()

			// All good
			return nil
		})
		return nil
	})
	if err != nil {
		return nil, err
	}

	if err := eg.Wait(); err != nil {
		return nil, err
	}
	res.Entities = found.ToSlice()

	// Sort the slice
	sort.Slice(res.Entities, func(i, j int) bool {
		return res.Entities[i].Name < res.Entities[j].Name
	})

	// All good
	return &res, nil
}

func parseAndReplaceReferencesInFile(
	ctx context.Context,
	f io.Reader,
	parser interfaces.Parser,
	rest interfaces.REST,
	cfg config.Config,
) (bool, string, error) {
	var contentBuilder strings.Builder
	var ret *interfaces.EntityRef

	modified := false

	// Compile the regular expression
	re, err := regexp.Compile(parser.GetRegex())
	if err != nil {
		return false, "", err
	}

	// Read the file line by line
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()

		// Skip commented lines
		if strings.HasPrefix(strings.TrimLeft(line, " \t\n\r"), "#") {
			// Write the line to the content builder buffer
			contentBuilder.WriteString(line + "\n")
			continue
		}

		// See if we can match an entity reference in the line
		newLine := re.ReplaceAllStringFunc(line, func(matchedLine string) string {
			// Modify the reference in the line
			ret, err = parser.Replace(ctx, matchedLine, rest, cfg)
			if err != nil {
				// Return the original line as we don't want to update it in case something errored out
				return matchedLine
			}
			// Construct the new line, comments in dockerfiles are handled differently than yml files
			if strings.Contains(matchedLine, "FROM") {
				return fmt.Sprintf("%s%s:%s@%s", ret.Prefix, ret.Name, ret.Tag, ret.Ref)
			}
			return fmt.Sprintf("%s%s@%s # %s", ret.Prefix, ret.Name, ret.Ref, ret.Tag)
		})

		// Check if the line was modified and set the modified flag to true if it was
		if newLine != line {
			modified = true
		}

		// Write the line to the content builder buffer
		contentBuilder.WriteString(newLine + "\n")
	}

	// Check for errors during the scan
	if err := scanner.Err(); err != nil {
		return false, "", err
	}

	// Return the workflow content
	return modified, contentBuilder.String(), nil
}

// listReferencesInFile takes the given file reader and returns a map of all references, action or images it finds
func listReferencesInFile(
	f io.Reader,
	parser interfaces.Parser,
) (mapset.Set[interfaces.EntityRef], error) {
	found := mapset.NewSet[interfaces.EntityRef]()

	// Compile the regular expression
	re, err := regexp.Compile(parser.GetRegex())
	if err != nil {
		return nil, err
	}

	// Read the file line by line
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()

		// Skip commented lines
		if strings.HasPrefix(strings.TrimLeft(line, " \t\n\r"), "#") {
			continue
		}

		// See if we can match an entity reference in the line
		foundEntries := re.FindAllString(line, -1)
		// nolint:gosimple
		if foundEntries != nil {
			for _, entry := range foundEntries {
				e, err := parser.ConvertToEntityRef(entry)
				if err != nil {
					continue
				}
				found.Add(*e)
			}
		}
	}

	// Check for errors during the scan
	if err := scanner.Err(); err != nil {
		return nil, err
	}

	// Return the found references
	return found, nil
}
07070100000028000081A400000000000000000000000167E26F5900008940000000000000000000000000000000000000002C00000000frizbee-0.1.7/pkg/replacer/replacer_test.go//
// Copyright 2024 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package replacer

import (
	"context"
	"os"
	"strings"
	"testing"

	"github.com/go-git/go-billy/v5/memfs"
	"github.com/stretchr/testify/require"

	"github.com/stacklok/frizbee/internal/cli"
	"github.com/stacklok/frizbee/pkg/interfaces"
	"github.com/stacklok/frizbee/pkg/replacer/actions"
	"github.com/stacklok/frizbee/pkg/replacer/image"
	"github.com/stacklok/frizbee/pkg/utils/config"
	"github.com/stacklok/frizbee/pkg/utils/ghrest"
)

func TestReplacer_ParseContainerImageString(t *testing.T) {
	t.Parallel()

	type args struct {
		refstr string
	}
	tests := []struct {
		name    string
		args    args
		want    *interfaces.EntityRef
		wantErr bool
	}{
		{
			name: "dockerfile - tag",
			args: args{
				refstr: "FROM golang:1.22.2",
			},
			want: &interfaces.EntityRef{
				Name:   "index.docker.io/library/golang",
				Ref:    "sha256:d5302d40dc5fbbf38ec472d1848a9d2391a13f93293a6a5b0b87c99dc0eaa6ae",
				Type:   image.ReferenceType,
				Tag:    "1.22.2",
				Prefix: "FROM ",
			},
			wantErr: false,
		},
		{
			name: "dockerfile - tag, stage and platform",
			args: args{
				refstr: "FROM --platform=linux/s390x golang:1.22.2 AS build",
			},
			want: &interfaces.EntityRef{
				Name:   "index.docker.io/library/golang",
				Ref:    "sha256:d5302d40dc5fbbf38ec472d1848a9d2391a13f93293a6a5b0b87c99dc0eaa6ae",
				Type:   image.ReferenceType,
				Tag:    "1.22.2",
				Prefix: "FROM --platform=linux/s390x ",
			},
			wantErr: false,
		},
		{
			name: "dockerfile - no tag",
			args: args{
				refstr: "FROM golang",
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "dockerfile - latest",
			args: args{
				refstr: "FROM golang:latest",
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "dockerfile - already by digest",
			args: args{
				refstr: "FROM golang:1.22.2@sha256:aca60c1f21de99aa3a34e653f0cdc8c8ea8fe6480359229809d5bcb974f599ec",
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "dockerfile - scratch",
			args: args{
				refstr: "FROM scratch",
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "valid 1",
			args: args{
				refstr: "ghcr.io/stacklok/minder/helm/minder:0.20231123.829_ref.26ca90b",
			},
			want: &interfaces.EntityRef{
				Name:   "ghcr.io/stacklok/minder/helm/minder",
				Ref:    "sha256:a29f8a8d28f0af7f70a4b3dd3e33c8c8cc5cf9e88e802e2700cf272a0b6140ec",
				Type:   image.ReferenceType,
				Tag:    "0.20231123.829_ref.26ca90b",
				Prefix: "",
			},
			wantErr: false,
		},
		{
			name: "valid 2",
			args: args{
				refstr: "devopsfaith/krakend:2.5.0",
			},
			want: &interfaces.EntityRef{
				Name:   "index.docker.io/devopsfaith/krakend",
				Ref:    "sha256:6a3c8e5e1a4948042bfb364ed6471e16b4a26d0afb6c3c01ebcb88b3fa551036",
				Type:   image.ReferenceType,
				Tag:    "2.5.0",
				Prefix: "",
			},
			wantErr: false,
		},
		{
			name: "image with no tag is skipped",
			args: args{
				refstr: "image: nginx",
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "image with latest tag is skipped",
			args: args{
				refstr: "image: nginx:latest",
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "invalid ref string",
			args: args{
				refstr: "ghcr.io/stacklok/minder/helm/minder!",
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "nonexistent container in nonexistent registry",
			args: args{
				refstr: "beeeeer.io/ipa/toppling-goliath/king-sue:1.0.0",
			},
			want:    nil,
			wantErr: true,
		},
		// TODO: Create a dedicated container image for this test and push it so that latest doesnt change
		//{
		//	name: "container reference with no tag or digest",
		//	args: args{
		//		refstr: "nginx",
		//	},
		//	want: &interfaces.EntityRef{
		//		Name:   "index.docker.io/library/nginx",
		//		Ref:    "sha256:faef0b115e699b1e70b1f9a939ea2bc62c26485f6b72e91c8a7b236f1f8589c1",
		//		Type:   image.ReferenceType,
		//		Tag:    "latest",
		//		Prefix: "",
		//	},
		//	wantErr: false,
		//},
		{
			name: "invalid reference with special characters",
			args: args{
				refstr: "nginx@#$$%%^&*",
			},
			want:    nil,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			ctx := context.Background()
			config := &config.Config{
				Images: config.Images{
					ImageFilter: config.ImageFilter{
						ExcludeTags: []string{"latest"},
					},
				},
			}
			r := NewContainerImagesReplacer(config)
			got, err := r.ParseString(ctx, tt.args.refstr)
			if tt.wantErr {
				require.Error(t, err)
				require.Empty(t, got)
				return
			}
			require.NoError(t, err)
			require.Equal(t, tt.want, got)
		})
	}
}

func TestReplacer_ParseGitHubActionString(t *testing.T) {
	t.Parallel()

	type args struct {
		action string
	}
	tests := []struct {
		name    string
		args    args
		want    *interfaces.EntityRef
		wantErr bool
	}{
		{
			name: "action using a container via docker://avtodev/markdown-lint:v1",
			args: args{
				action: "uses: docker://avtodev/markdown-lint:v1",
			},
			want: &interfaces.EntityRef{
				Name:   "index.docker.io/avtodev/markdown-lint",
				Ref:    "sha256:6aeedc2f49138ce7a1cd0adffc1b1c0321b841dc2102408967d9301c031949ee",
				Type:   image.ReferenceType,
				Tag:    "v1",
				Prefix: "uses: docker://",
			},
			wantErr: false,
		},
		{
			name: "actions/checkout with v4.1.1",
			args: args{
				action: "actions/checkout@v4.1.1",
			},
			want: &interfaces.EntityRef{
				Name:   "actions/checkout",
				Ref:    "b4ffde65f46336ab88eb53be808477a3936bae11",
				Type:   actions.ReferenceType,
				Tag:    "v4.1.1",
				Prefix: "",
			},
			wantErr: false,
		},
		{
			name: "actions/checkout with v3.6.0",
			args: args{
				action: "uses: actions/checkout@v3.6.0",
			},
			want: &interfaces.EntityRef{
				Name:   "actions/checkout",
				Ref:    "f43a0e5ff2bd294095638e18286ca9a3d1956744",
				Type:   actions.ReferenceType,
				Tag:    "v3.6.0",
				Prefix: "uses: ",
			},
			wantErr: false,
		},
		{
			name: "actions/checkout with checksum returns checksum",
			args: args{
				action: "actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f",
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "aquasecurity/trivy-action with 0.14.0",
			args: args{
				action: "aquasecurity/trivy-action@0.14.0",
			},
			want: &interfaces.EntityRef{
				Name:   "aquasecurity/trivy-action",
				Ref:    "2b6a709cf9c4025c5438138008beaddbb02086f0",
				Type:   actions.ReferenceType,
				Tag:    "0.14.0",
				Prefix: "",
			},
			wantErr: false,
		},
		{
			name: "aquasecurity/trivy-action with branch returns checksum",
			args: args{
				action: "aquasecurity/trivy-action@bump-trivy",
			},
			want: &interfaces.EntityRef{
				Name:   "aquasecurity/trivy-action",
				Ref:    "fb5e1b36be448e92ca98648c661bd7e9da1f1317",
				Type:   actions.ReferenceType,
				Tag:    "bump-trivy",
				Prefix: "",
			},
			wantErr: false,
		},
		{
			name: "aquasecurity/trivy-action with ignored branch returns error",
			args: args{
				action: "aquasecurity/trivy-action@main",
			},
			wantErr: true,
		},
		{
			name: "actions/checkout with invalid tag returns error",
			args: args{
				action: "actions/checkout@v4.1.1.1",
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "actions/checkout with invalid action returns error",
			args: args{
				action: "invalid-action@v4.1.1",
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "actions/checkout with empty action returns error",
			args: args{
				action: "@v4.1.1",
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "actions/checkout with empty tag returns error",
			args: args{
				action: "actions/checkout",
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "actions/setup-node with v1 is an array",
			args: args{
				action: "actions/setup-node@v1",
			},
			want: &interfaces.EntityRef{
				Name:   "actions/setup-node",
				Ref:    "f1f314fca9dfce2769ece7d933488f076716723e",
				Type:   actions.ReferenceType,
				Tag:    "v1",
				Prefix: "",
			},
		},
		{
			name: "anchore/sbom-action/download-syft with a sub-action works",
			args: args{
				action: "anchore/sbom-action/download-syft@v0.14.3",
			},
			want: &interfaces.EntityRef{
				Name:   "anchore/sbom-action/download-syft",
				Ref:    "78fc58e266e87a38d4194b2137a3d4e9bcaf7ca1",
				Type:   actions.ReferenceType,
				Tag:    "v0.14.3",
				Prefix: "",
			},
		},
		{
			name: "invalid action reference",
			args: args{
				action: "invalid-reference",
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "missing action tag",
			args: args{
				action: "actions/checkout",
			},
			want:    nil,
			wantErr: true,
		},
		{
			name: "action with special characters",
			args: args{
				action: "actions/checkout@#$$%%^&*",
			},
			want:    nil,
			wantErr: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			ctx := context.Background()
			conf := &config.Config{
				GHActions: config.GHActions{
					Filter: config.Filter{
						ExcludeBranches: []string{"main"},
					},
				},
				Images: config.Images{
					ImageFilter: config.ImageFilter{
						ExcludeTags: []string{"latest"},
					},
				},
			}
			r := NewGitHubActionsReplacer(conf).WithGitHubClientFromToken(os.Getenv("GITHUB_TOKEN"))
			got, err := r.ParseString(ctx, tt.args.action)
			if tt.wantErr {
				require.Error(t, err)
				require.Empty(t, got)
				return
			}
			require.NoError(t, err)
			require.Equal(t, tt.want, got)
		})
	}
}

func TestReplacer_ParseContainerImagesInFile(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name     string
		before   string
		expected string
		modified bool
		wantErr  bool
	}{
		{
			name: "Replace image reference",
			before: `
version: v1
services:
  - name: kube-apiserver
    image: registry.k8s.io/kube-apiserver:v1.20.0
  - name: kube-controller-manager
    image: registry.k8s.io/kube-controller-manager:v1.15.0
  - name: minder-app
    image: minder:latest
`,
			expected: `
version: v1
services:
  - name: kube-apiserver
    image: registry.k8s.io/kube-apiserver@sha256:8b8125d7a6e4225b08f04f65ca947b27d0cc86380bf09fab890cc80408230114 # v1.20.0
  - name: kube-controller-manager
    image: registry.k8s.io/kube-controller-manager@sha256:835f32a5cdb30e86f35675dd91f9c7df01d48359ab8b51c1df866a2c7ea2e870 # v1.15.0
  - name: minder-app
    image: minder:latest
`,
			modified: true,
		},
		{
			name: "No image reference modification",
			before: `
version: v1
services:
  - name: minder-app
    image: minder:latest
`,
			expected: `
version: v1
services:
  - name: minder-app
    image: minder:latest
`,
			modified: false,
		},
		{
			name: "Invalid image reference format",
			before: `
version: v1
services:
  - name: invalid-service
    image: invalid@@reference
`,
			expected: `
version: v1
services:
  - name: invalid-service
    image: invalid@@reference
`,
			modified: false,
			wantErr:  false,
		},
		{
			name: "Multiple valid image references with one commented",
			before: `
version: v1
services:
  - name: kube-apiserver
    image: registry.k8s.io/kube-apiserver:v1.20.0
  - name: kube-controller-manager
    image: registry.k8s.io/kube-controller-manager:v1.15.0
  - name: minder-app
    image: minder:latest
  # - name: nginx
  #  image: nginx:latest
`,
			expected: `
version: v1
services:
  - name: kube-apiserver
    image: registry.k8s.io/kube-apiserver@sha256:8b8125d7a6e4225b08f04f65ca947b27d0cc86380bf09fab890cc80408230114 # v1.20.0
  - name: kube-controller-manager
    image: registry.k8s.io/kube-controller-manager@sha256:835f32a5cdb30e86f35675dd91f9c7df01d48359ab8b51c1df866a2c7ea2e870 # v1.15.0
  - name: minder-app
    image: minder:latest
  # - name: nginx
  #  image: nginx:latest
`,
			modified: true,
		},
		{
			name: "Valid image reference without specifying the tag",
			before: `
apiVersion: v1
kind: Pod
metadata:
  name: mount-host
  namespace: playground
spec:
  containers:
  - name: mount-host
    image: alpine
    command: ["sleep"]
    args: ["infinity"]
    volumeMounts:
    - name: host-root
      mountPath: /host
      readOnly: true
  volumes:
  - name: host-root
    hostPath:
      path: /
      type: Directory
`,
			modified: false,
		},
		{
			name: "A complex dockerfile",
			before: `
ARG BASE_IMAGE=alpine

FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.2.1@sha256:8879a398dedf0aadaacfbd332b29ff2f84bc39ae6d4e9c0a1109db27ac5ba012 AS xx

FROM --platform=$BUILDPLATFORM golang:1.20.4-alpine3.16 AS builder

COPY --from=xx / /

RUN apk add --update alpine-sdk ca-certificates openssl clang lld

ARG TARGETPLATFORM

RUN xx-apk --update add musl-dev gcc

# lld has issues building static binaries for ppc so prefer ld for it
RUN [ "$(xx-info arch)" != "ppc64le" ] || XX_CC_PREFER_LINKER=ld xx-clang --setup-target-triple

RUN xx-go --wrap

WORKDIR /usr/local/src/dex

ARG GOPROXY

ENV CGO_ENABLED=1

COPY go.mod go.sum ./
COPY api/v2/go.mod api/v2/go.sum ./api/v2/
RUN go mod download

COPY . .

RUN make release-binary
RUN xx-verify /go/bin/dex && xx-verify /go/bin/docker-entrypoint

FROM alpine:3.18.2 AS stager

RUN mkdir -p /var/dex
RUN mkdir -p /etc/dex
COPY config.docker.yaml /etc/dex/

FROM alpine:3.18.2 AS gomplate

ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT

ENV GOMPLATE_VERSION=v3.11.4

RUN wget -O /usr/local/bin/gomplate \
  "https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS:-linux}-${TARGETARCH:-amd64}${TARGETVARIANT}" \
  && chmod +x /usr/local/bin/gomplate

# For Dependabot to detect base image versions
FROM alpine:3.18.2 AS alpine
FROM gcr.io/distroless/static:latest AS distroless

FROM $BASE_IMAGE

# Dex connectors, such as GitHub and Google logins require root certificates.
# Proper installations should manage those certificates, but it's a bad user
# experience when this doesn't work out of the box.
#
# See https://go.dev/src/crypto/x509/root_linux.go for Go root CA bundle locations.
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt

COPY --from=stager --chown=1001:1001 /var/dex /var/dex
COPY --from=stager --chown=1001:1001 /etc/dex /etc/dex

# Copy module files for CVE scanning / dependency analysis.
COPY --from=builder /usr/local/src/dex/go.mod /usr/local/src/dex/go.sum /usr/local/src/dex/
COPY --from=builder /usr/local/src/dex/api/v2/go.mod /usr/local/src/dex/api/v2/go.sum /usr/local/src/dex/api/v2/

COPY --from=builder /go/bin/dex /usr/local/bin/dex
COPY --from=builder /go/bin/docker-entrypoint /usr/local/bin/docker-entrypoint
COPY --from=builder /usr/local/src/dex/web /srv/dex/web

COPY --from=gomplate /usr/local/bin/gomplate /usr/local/bin/gomplate

USER 1001:1001

ENTRYPOINT ["/usr/local/bin/docker-entrypoint"]
CMD ["dex", "serve", "/etc/dex/config.docker.yaml"]
`,
			expected: `
ARG BASE_IMAGE=alpine

FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.2.1@sha256:8879a398dedf0aadaacfbd332b29ff2f84bc39ae6d4e9c0a1109db27ac5ba012 AS xx

FROM --platform=$BUILDPLATFORM index.docker.io/library/golang:1.20.4-alpine3.16@sha256:6469405d7297f82d56195c90a3270b0806ef4bd897aa0628477d9959ab97a577 AS builder

COPY --from=xx / /

RUN apk add --update alpine-sdk ca-certificates openssl clang lld

ARG TARGETPLATFORM

RUN xx-apk --update add musl-dev gcc

# lld has issues building static binaries for ppc so prefer ld for it
RUN [ "$(xx-info arch)" != "ppc64le" ] || XX_CC_PREFER_LINKER=ld xx-clang --setup-target-triple

RUN xx-go --wrap

WORKDIR /usr/local/src/dex

ARG GOPROXY

ENV CGO_ENABLED=1

COPY go.mod go.sum ./
COPY api/v2/go.mod api/v2/go.sum ./api/v2/
RUN go mod download

COPY . .

RUN make release-binary
RUN xx-verify /go/bin/dex && xx-verify /go/bin/docker-entrypoint

FROM index.docker.io/library/alpine:3.18.2@sha256:82d1e9d7ed48a7523bdebc18cf6290bdb97b82302a8a9c27d4fe885949ea94d1 AS stager

RUN mkdir -p /var/dex
RUN mkdir -p /etc/dex
COPY config.docker.yaml /etc/dex/

FROM index.docker.io/library/alpine:3.18.2@sha256:82d1e9d7ed48a7523bdebc18cf6290bdb97b82302a8a9c27d4fe885949ea94d1 AS gomplate

ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT

ENV GOMPLATE_VERSION=v3.11.4

RUN wget -O /usr/local/bin/gomplate \
  "https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS:-linux}-${TARGETARCH:-amd64}${TARGETVARIANT}" \
  && chmod +x /usr/local/bin/gomplate

# For Dependabot to detect base image versions
FROM index.docker.io/library/alpine:3.18.2@sha256:82d1e9d7ed48a7523bdebc18cf6290bdb97b82302a8a9c27d4fe885949ea94d1 AS alpine
FROM gcr.io/distroless/static:latest AS distroless

FROM $BASE_IMAGE

# Dex connectors, such as GitHub and Google logins require root certificates.
# Proper installations should manage those certificates, but it's a bad user
# experience when this doesn't work out of the box.
#
# See https://go.dev/src/crypto/x509/root_linux.go for Go root CA bundle locations.
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt

COPY --from=stager --chown=1001:1001 /var/dex /var/dex
COPY --from=stager --chown=1001:1001 /etc/dex /etc/dex

# Copy module files for CVE scanning / dependency analysis.
COPY --from=builder /usr/local/src/dex/go.mod /usr/local/src/dex/go.sum /usr/local/src/dex/
COPY --from=builder /usr/local/src/dex/api/v2/go.mod /usr/local/src/dex/api/v2/go.sum /usr/local/src/dex/api/v2/

COPY --from=builder /go/bin/dex /usr/local/bin/dex
COPY --from=builder /go/bin/docker-entrypoint /usr/local/bin/docker-entrypoint
COPY --from=builder /usr/local/src/dex/web /srv/dex/web

COPY --from=gomplate /usr/local/bin/gomplate /usr/local/bin/gomplate

USER 1001:1001

ENTRYPOINT ["/usr/local/bin/docker-entrypoint"]
CMD ["dex", "serve", "/etc/dex/config.docker.yaml"]
`,
			modified: true,
		},
	}

	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			ctx := context.Background()
			r := NewContainerImagesReplacer(&config.Config{
				Images: config.Images{
					ImageFilter: config.ImageFilter{
						ExcludeTags: []string{"latest"},
					},
				},
			})
			modified, newContent, err := r.ParseFile(ctx, strings.NewReader(tt.before))
			if tt.wantErr {
				require.False(t, modified)
				require.Equal(t, tt.before, newContent)
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			if tt.modified {
				require.True(t, modified)
				if tt.expected != "" {
					require.Equal(t, tt.expected, newContent)
				} else {
					require.NotEmpty(t, tt.before, newContent)
				}
			} else {
				require.False(t, modified)
				require.Equal(t, tt.before, newContent)
			}

		})
	}
}

func TestReplacer_ParseGitHubActionsInFile(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name           string
		before         string
		expected       string
		regex          string
		modified       bool
		wantErr        bool
		useCustomRegex bool
	}{
		{
			name: "Replace image reference",
			before: `
name: Linter
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: ./minder/server.yml # this should not be replaced
      - uses: actions/checkout@v2
      - uses: xt0rted/markdownlint-problem-matcher@v1
      - name: "Run Markdown linter"
        uses: docker://avtodev/markdown-lint:v1
        with:
          args: src/*.md
`,
			expected: `
name: Linter
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: ./minder/server.yml # this should not be replaced
      - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2
      - uses: xt0rted/markdownlint-problem-matcher@b643b0751c371f357690337d4549221347c0e1bc # v1
      - name: "Run Markdown linter"
        uses: docker://index.docker.io/avtodev/markdown-lint@sha256:6aeedc2f49138ce7a1cd0adffc1b1c0321b841dc2102408967d9301c031949ee # v1
        with:
          args: src/*.md
`,
			modified: true,
			wantErr:  false,
		},
		{
			name: "Replace actions with tags, not with branches",
			before: `
name: Linter
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: ./minder/server.yml # this should not be replaced
      - uses: actions/checkout@v2
      - uses: aquasecurity/trivy-action@main
      - name: "Run Markdown linter"
        uses: docker://avtodev/markdown-lint:v1
        with:
          args: src/*.md
`,
			expected: `
name: Linter
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: ./minder/server.yml # this should not be replaced
      - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2
      - uses: aquasecurity/trivy-action@main
      - name: "Run Markdown linter"
        uses: docker://index.docker.io/avtodev/markdown-lint@sha256:6aeedc2f49138ce7a1cd0adffc1b1c0321b841dc2102408967d9301c031949ee # v1
        with:
          args: src/*.md
`,
			modified: true,
			wantErr:  false,
		},
		{
			name: "No action reference modification",
			before: `
name: Linter
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: ./minder/server.yml # this should not be replaced
      # - uses: actions/checkout@v2
`,
			expected: `
name: Linter
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: ./minder/server.yml # this should not be replaced
      # - uses: actions/checkout@v2
`,
			modified: false,
		},
		{
			name: "Invalid action reference format",
			before: `
name: Linter
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: invalid@@reference
`,
			expected: `
name: Linter
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: invalid@@reference
`,
			modified: false,
			wantErr:  false,
		},
		{
			name: "Multiple valid action references",
			before: `
name: Linter
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: ./minder/server.yml # this should not be replaced
      - uses: actions/checkout@v2
      - uses: xt0rted/markdownlint-problem-matcher@v1
`,
			expected: `
name: Linter
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: ./minder/server.yml # this should not be replaced
      - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2
      - uses: xt0rted/markdownlint-problem-matcher@b643b0751c371f357690337d4549221347c0e1bc # v1
`,
			modified: true,
		},
		{
			name: "Fail with custom regex",
			before: `
name: Linter
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: ./minder/server.yml # this should not be replaced
      - uses: actions/checkout@v2
      - uses: xt0rted/markdownlint-problem-matcher@v1
      - name: "Run Markdown linter"
        uses: docker://avtodev/markdown-lint:v1
        with:
          args: src/*.md
`,
			expected: `
name: Linter
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: ./minder/server.yml # this should not be replaced
      - uses: actions/checkout@v2
      - uses: xt0rted/markdownlint-problem-matcher@v1
      - name: "Run Markdown linter"
        uses: docker://avtodev/markdown-lint:v1
        with:
          args: src/*.md
`,
			modified:       false,
			wantErr:        false,
			regex:          "invalid-regexp",
			useCustomRegex: true,
		},
	}
	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			ctx := context.Background()
			r := NewGitHubActionsReplacer(&config.Config{
				GHActions: config.GHActions{
					Filter: config.Filter{
						ExcludeBranches: []string{"*"},
					},
				},
			}).WithGitHubClientFromToken(os.Getenv(cli.GitHubTokenEnvKey))
			if tt.useCustomRegex {
				r = r.WithUserRegex(tt.regex)
			}
			modified, newContent, err := r.ParseFile(ctx, strings.NewReader(tt.before))
			if tt.modified {
				require.True(t, modified)
				require.Equal(t, tt.expected, newContent)
			} else {
				require.False(t, modified)
				require.Equal(t, tt.before, newContent)
			}
			if tt.wantErr {
				require.False(t, modified)
				require.Equal(t, tt.before, newContent)
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			require.Equal(t, tt.expected, newContent)
		})
	}
}

func TestReplacer_NewGitHubActionsReplacer(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{}
	tests := []struct {
		name string
		cfg  *config.Config
	}{
		{name: "valid config", cfg: cfg},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			r := NewGitHubActionsReplacer(tt.cfg)
			require.NotNil(t, r)
			require.IsType(t, &Replacer{}, r)
			require.IsType(t, actions.New(), r.parser)
		})
	}
}

func TestReplacer_NewContainerImagesReplacer(t *testing.T) {
	t.Parallel()

	cfg := &config.Config{}
	tests := []struct {
		name string
		cfg  *config.Config
	}{
		{name: "valid config", cfg: cfg},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			r := NewContainerImagesReplacer(tt.cfg)
			require.NotNil(t, r)
			require.IsType(t, &Replacer{}, r)
			require.IsType(t, image.New(), r.parser)
		})
	}
}

func TestReplacer_WithGitHubClient(t *testing.T) {
	t.Parallel()

	r := &Replacer{}
	tests := []struct {
		name  string
		token string
	}{
		{name: "valid token", token: "valid_token"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			r = r.WithGitHubClientFromToken(tt.token)
			require.NotNil(t, r)
			require.IsType(t, ghrest.NewClient(tt.token), r.rest)
		})
	}
}

func TestReplacer_WithUserRegex(t *testing.T) {
	t.Parallel()

	r := &Replacer{parser: actions.New()}
	tests := []struct {
		name  string
		regex string
	}{
		{name: "valid regex", regex: `^test-regex$`},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			r = r.WithUserRegex(tt.regex)
			require.Equal(t, tt.regex, r.parser.GetRegex())
		})
	}
}

func TestReplacer_WithCacheDisabled(t *testing.T) {
	t.Parallel()

	r := &Replacer{parser: actions.New()}
	tests := []struct {
		name string
	}{
		{name: "disable cache"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			r = r.WithCacheDisabled()
			// we don't test if this passed here because it's an internal implementation detail
			// but let's ensure we don't panic for some reason
		})
	}
}

func TestReplacer_ParsePathInFS(t *testing.T) {
	t.Parallel()

	r := &Replacer{parser: actions.New(), cfg: config.Config{}}
	fs := memfs.New()
	tests := []struct {
		name    string
		base    string
		wantErr bool
	}{
		{name: "valid base", base: "some-base", wantErr: false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			_, err := r.ParsePathInFS(context.Background(), fs, tt.base)
			if tt.wantErr {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestReplacer_ListPathInFS(t *testing.T) {
	t.Parallel()

	r := &Replacer{parser: actions.New(), cfg: config.Config{}}
	fs := memfs.New()
	tests := []struct {
		name    string
		base    string
		wantErr bool
	}{
		{name: "valid base", base: "some-base", wantErr: false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			_, err := r.ListPathInFS(fs, tt.base)
			if tt.wantErr {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
			}
		})
	}
}

func TestReplacer_ListContainerImagesInFile(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name           string
		before         string
		expected       *ListResult
		regex          string
		wantErr        bool
		useCustomRegex bool
	}{
		{
			name: "Lust image reference",
			before: `
version: v1
services:
 - name: kube-apiserver
   image: registry.k8s.io/kube-apiserver:v1.20.0
 - name: kube-controller-manager
   image: registry.k8s.io/kube-controller-manager:v1.15.0
 - name: minder-app
   image: minder:latest
`,
			expected: &ListResult{
				Entities: []interfaces.EntityRef{
					{
						Name: "registry.k8s.io/kube-apiserver",
						Ref:  "v1.20.0",
						Type: image.ReferenceType,
					},
					{
						Name: "registry.k8s.io/kube-controller-manager",
						Ref:  "v1.15.0",
						Type: image.ReferenceType,
					},
					{
						Name: "minder",
						Ref:  "latest",
						Type: image.ReferenceType,
					},
				},
			},
			wantErr: false,
		},
		{
			name: "No image reference modification",
			before: `
		version: v1
		services:
		- name: minder-app
		  # image: minder:latest
		`,
			expected: &ListResult{
				Entities: []interfaces.EntityRef{},
			},
			wantErr: false,
		},
		{
			name: "Invalid image reference format",
			before: `
		version: v1
		services:
		- name: invalid-service
		  image: invalid@@reference
		`,
			expected: &ListResult{
				Entities: []interfaces.EntityRef{},
			},
			wantErr: false,
		},
		{
			name: "Multiple valid image references with one commented",
			before: `
		version: v1
		services:
		- name: kube-apiserver
		  image: registry.k8s.io/kube-apiserver@sha256:8b8125d7a6e4225b08f04f65ca947b27d0cc86380bf09fab890cc80408230114 # v1.20.0
		- name: kube-controller-manager
		  image: registry.k8s.io/kube-controller-manager@sha256:835f32a5cdb30e86f35675dd91f9c7df01d48359ab8b51c1df866a2c7ea2e870 # v1.15.0
		- name: minder-app
		  image: minder:latest
		# - name: nginx
		#  image: nginx:latest
		`,
			expected: &ListResult{
				Entities: []interfaces.EntityRef{
					{
						Name: "registry.k8s.io/kube-apiserver",
						Ref:  "sha256:8b8125d7a6e4225b08f04f65ca947b27d0cc86380bf09fab890cc80408230114",
						Type: image.ReferenceType,
					},
					{
						Name: "registry.k8s.io/kube-controller-manager",
						Ref:  "sha256:835f32a5cdb30e86f35675dd91f9c7df01d48359ab8b51c1df866a2c7ea2e870",
						Type: image.ReferenceType,
					},
					{
						Name: "minder",
						Ref:  "latest",
						Type: image.ReferenceType,
					},
				},
			},
		},
		{
			name: "Valid image reference without specifying the tag",
			before: `
apiVersion: v1
kind: Pod
metadata:
  name: mount-host
  namespace: playground
spec:
  containers:
  - name: mount-host
    image: alpine
    command: ["sleep"]
    args: ["infinity"]
    volumeMounts:
    - name: host-root
      mountPath: /host
      readOnly: true
  volumes:
  - name: host-root
    hostPath:
      path: /
      type: Directory
		`,
			expected: &ListResult{
				Entities: []interfaces.EntityRef{
					{
						Name: "alpine",
						Ref:  "latest",
						Type: image.ReferenceType,
					},
				},
			},
		},
	}
	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			r := NewContainerImagesReplacer(&config.Config{})
			listRes, err := r.ListInFile(strings.NewReader(tt.before))
			if tt.wantErr {
				require.Nil(t, listRes)
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			require.Equal(t, len(tt.expected.Entities), len(listRes.Entities))
			for _, entity := range tt.expected.Entities {
				require.Contains(t, listRes.Entities, entity)
			}
		})
	}
}

func TestReplacer_ListGitHubActionsInFile(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name           string
		before         string
		expected       *ListResult
		regex          string
		wantErr        bool
		useCustomRegex bool
	}{
		{
			name: "List image reference",
			before: `
name: Linter
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: ./minder/server.yml # this should not be listed
      - uses: actions/checkout@v2
      - uses: xt0rted/markdownlint-problem-matcher@v1
      - name: "Run Markdown linter"
        uses: docker://avtodev/markdown-lint:v1
        with:
          args: src/*.md
`,
			expected: &ListResult{
				Entities: []interfaces.EntityRef{
					{
						Name: "actions/checkout",
						Ref:  "v2",
						Type: actions.ReferenceType,
					},
					{
						Name: "xt0rted/markdownlint-problem-matcher",
						Ref:  "v1",
						Type: actions.ReferenceType,
					},
					{
						Name: "avtodev/markdown-lint",
						Ref:  "v1",
						Type: image.ReferenceType,
					},
				},
			},
			wantErr: false,
		},
		{
			name: "No action references",
			before: `
		name: Linter
		on: pull_request
		jobs:
		 build:
		   runs-on: ubuntu-latest
		   steps:
		     - uses: ./minder/server.yml # this should not be replaced
		     # - uses: actions/checkout@v2
		`,
			expected: &ListResult{
				Entities: []interfaces.EntityRef{},
			},
			wantErr: false,
		},
		{
			name: "Invalid action reference format",
			before: `
		name: Linter
		on: pull_request
		jobs:
		 build:
		   runs-on: ubuntu-latest
		   steps:
		     - uses: invalid@@reference
		`,
			expected: &ListResult{
				Entities: []interfaces.EntityRef{},
			},
			wantErr: false,
		},
		{
			name: "Multiple valid action references",
			before: `
		name: Linter
		on: pull_request
		jobs:
		 build:
		   runs-on: ubuntu-latest
		   steps:
		     - uses: ./minder/server.yml # this should not be replaced
		     - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2
		     - uses: xt0rted/markdownlint-problem-matcher@b643b0751c371f357690337d4549221347c0e1bc # v1
		`,
			expected: &ListResult{
				Entities: []interfaces.EntityRef{
					{
						Name: "actions/checkout",
						Ref:  "ee0669bd1cc54295c223e0bb666b733df41de1c5",
						Type: actions.ReferenceType,
					},
					{
						Name: "xt0rted/markdownlint-problem-matcher",
						Ref:  "b643b0751c371f357690337d4549221347c0e1bc",
						Type: actions.ReferenceType,
					},
				},
			},
		},
		{
			name: "Fail with custom regex",
			before: `
		name: Linter
		on: pull_request
		jobs:
		 build:
		   runs-on: ubuntu-latest
		   steps:
		     - uses: ./minder/server.yml # this should not be replaced
		     - uses: actions/checkout@v2
		     - uses: xt0rted/markdownlint-problem-matcher@v1
		     - name: "Run Markdown linter"
		       uses: docker://avtodev/markdown-lint:v1
		       with:
		         args: src/*.md
		`,
			expected: &ListResult{
				Entities: []interfaces.EntityRef{},
			},
			wantErr:        false,
			regex:          "invalid-regexp",
			useCustomRegex: true,
		},
	}
	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			r := NewGitHubActionsReplacer(&config.Config{}).WithGitHubClientFromToken(os.Getenv(cli.GitHubTokenEnvKey))
			if tt.useCustomRegex {
				r = r.WithUserRegex(tt.regex)
			}
			listRes, err := r.ListInFile(strings.NewReader(tt.before))
			if tt.wantErr {
				require.Nil(t, listRes)
				require.Error(t, err)
				return
			}
			require.NoError(t, err)
			require.Equal(t, len(tt.expected.Entities), len(listRes.Entities))
			for _, entity := range tt.expected.Entities {
				require.Contains(t, listRes.Entities, entity)
			}
		})
	}
}
07070100000029000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001800000000frizbee-0.1.7/pkg/utils0707010000002A000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001F00000000frizbee-0.1.7/pkg/utils/config0707010000002B000081A400000000000000000000000167E26F590000114D000000000000000000000000000000000000002900000000frizbee-0.1.7/pkg/utils/config/config.go//
// Copyright 2024 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package config provides the frizbee configuration.
package config

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"slices"

	"github.com/go-git/go-billy/v5"
	"github.com/go-git/go-billy/v5/osfs"
	"github.com/spf13/cobra"
	"gopkg.in/yaml.v3"
)

type contextConfigKey struct{}

// ContextConfigKey is the context key for the configuration.
// nolint:gochecknoglobals // this is a context key
var ContextConfigKey = contextConfigKey{}

var (
	// ErrNoConfigInContext is returned when no configuration is found in the context.
	ErrNoConfigInContext = errors.New("no configuration found in context")
)

// FromCommand returns the configuration from the cobra command.
func FromCommand(cmd *cobra.Command) (*Config, error) {
	ctx := cmd.Context()
	cfg, ok := ctx.Value(ContextConfigKey).(*Config)
	if !ok {
		return nil, ErrNoConfigInContext
	}

	// If the platform flag is set, override the platform in the configuration.
	if cmd.Flags().Lookup("platform") != nil {
		cfg.Platform = cmd.Flag("platform").Value.String()
	}
	return cfg, nil
}

// Config is the frizbee configuration.
type Config struct {
	Platform  string    `yaml:"platform" mapstructure:"platform"`
	GHActions GHActions `yaml:"ghactions" mapstructure:"ghactions"`
	Images    Images    `yaml:"images" mapstructure:"images"`
}

// GHActions is the GitHub Actions configuration.
type GHActions struct {
	Filter `yaml:",inline" mapstructure:",inline"`
}

// Filter is a common configuration for filtering out patterns.
type Filter struct {
	// Exclude is a list of patterns to exclude.
	Exclude         []string `yaml:"exclude" mapstructure:"exclude"`
	ExcludeBranches []string `yaml:"exclude_branches" mapstructure:"exclude_branches"`
}

// Images is the image configuration.
type Images struct {
	ImageFilter `yaml:",inline" mapstructure:",inline"`
}

// ImageFilter is the image filter configuration.
type ImageFilter struct {
	// ExcludeImages is a regex that must match in order for an image to be excluded and not pinned
	ExcludeImages []string `yaml:"exclude_images" mapstructure:"exclude_images"`
	ExcludeTags   []string `yaml:"exclude_tags" mapstructure:"exclude_tags"`
}

// ParseConfigFile parses a configuration file.
func ParseConfigFile(configfile string) (*Config, error) {
	bfs := osfs.New(".")
	return ParseConfigFileFromFS(bfs, configfile)
}

// DefaultConfig returns the default configuration.
func DefaultConfig() *Config {
	return &Config{
		GHActions: GHActions{
			Filter: Filter{
				ExcludeBranches: []string{"main", "master"},
			},
		},
		Images: Images{
			ImageFilter: ImageFilter{
				ExcludeImages: []string{"scratch"},
				ExcludeTags:   []string{"latest"},
			},
		},
	}
}

// MergeUserConfig merges the user configuration with the default configuration.
// mostly making sure that we don't try to pin the scratch image
func MergeUserConfig(userConfig *Config) *Config {
	if userConfig == nil {
		return DefaultConfig()
	}

	if userConfig.Images.ExcludeImages == nil {
		userConfig.Images.ExcludeImages = []string{"scratch"}
	}

	if !slices.Contains(userConfig.Images.ExcludeImages, "scratch") {
		userConfig.Images.ExcludeImages = append(userConfig.Images.ExcludeImages, "scratch")
	}

	return userConfig
}

// ParseConfigFileFromFS parses a configuration file from a filesystem.
func ParseConfigFileFromFS(fs billy.Filesystem, configfile string) (*Config, error) {
	cfg := DefaultConfig()
	cleancfgfile := filepath.Clean(configfile)
	cfgF, err := fs.Open(cleancfgfile)
	if err != nil {
		if os.IsNotExist(err) {
			return cfg, nil
		}

		return nil, fmt.Errorf("failed to open config file: %w", err)
	}
	defer cfgF.Close() // nolint:errcheck

	dec := yaml.NewDecoder(cfgF)
	if err := dec.Decode(cfg); err != nil {
		if err != io.EOF {
			return nil, fmt.Errorf("failed to decode config file: %w", err)
		}
	}

	return cfg, nil
}
0707010000002C000081A400000000000000000000000167E26F59000010DC000000000000000000000000000000000000002E00000000frizbee-0.1.7/pkg/utils/config/config_test.gopackage config

import (
	"context"
	"testing"

	"github.com/go-git/go-billy/v5/memfs"
	"github.com/spf13/cobra"
	"github.com/stretchr/testify/require"
)

func TestFromCommand(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name         string
		contextCfg   *Config
		platformFlag string
		expectedCfg  *Config
		expectError  bool
	}{
		{
			name:        "NoConfigInContext",
			contextCfg:  nil,
			expectError: true,
		},
		{
			name:        "WithConfigInContext",
			contextCfg:  &Config{Platform: "linux/arm64"},
			expectedCfg: &Config{Platform: "linux/arm64"},
		},
		{
			name:         "WithPlatformFlag",
			contextCfg:   &Config{Platform: "linux/amd64"},
			platformFlag: "windows/arm64",
			expectedCfg:  &Config{Platform: "windows/arm64"},
		},
	}

	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			ctx := context.Background()

			cmd := &cobra.Command{}
			if tt.contextCfg != nil {
				ctx := context.WithValue(ctx, ContextConfigKey, tt.contextCfg)
				cmd.SetContext(ctx)
			} else {
				cmd.SetContext(ctx)
			}
			if tt.platformFlag != "" {
				cmd.Flags().String("platform", "", "platform")
				require.NoError(t, cmd.Flags().Set("platform", tt.platformFlag))
			}

			cfg, err := FromCommand(cmd)
			if tt.expectError {
				require.Error(t, err)
				require.Nil(t, cfg)
			} else {
				require.NoError(t, err)
				require.Equal(t, tt.expectedCfg, cfg)
			}
		})
	}
}

func TestParseConfigFile(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name           string
		fsContent      map[string]string
		fileName       string
		expectedResult *Config
		expectError    bool
	}{
		{
			name:           "FileNotFound",
			fileName:       "nonexistent.yaml",
			expectedResult: DefaultConfig(),
		},
		{
			name:        "InvalidYaml",
			fileName:    "invalid.yaml",
			fsContent:   map[string]string{"invalid.yaml": "invalid yaml content"},
			expectError: true,
		},
		{
			name:     "DontIgnoreBranches",
			fileName: "dont_ignore_branches.yaml",
			fsContent: map[string]string{
				"dont_ignore_branches.yaml": `
platform: linux/amd64
ghactions:
  exclude_branches:
`,
			},
			expectedResult: &Config{
				Platform: "linux/amd64",
				GHActions: GHActions{
					Filter: Filter{
						ExcludeBranches: []string{},
					},
				},
				Images: Images{
					ImageFilter: ImageFilter{
						ExcludeImages: []string{"scratch"},
						ExcludeTags:   []string{"latest"},
					},
				},
			},
		},
		{
			name:     "ValidYaml",
			fileName: "valid.yaml",
			fsContent: map[string]string{
				"valid.yaml": `
platform: linux/amd64
ghactions:
  exclude:
    - pattern1
    - pattern2
images:
  exclude_images:
    - notthisone
  exclude_tags:
    - notthistag
`,
			},
			expectedResult: &Config{
				Platform: "linux/amd64",
				GHActions: GHActions{
					Filter: Filter{
						Exclude:         []string{"pattern1", "pattern2"},
						ExcludeBranches: []string{"main", "master"},
					},
				},
				Images: Images{
					ImageFilter: ImageFilter{
						ExcludeImages: []string{"notthisone"},
						ExcludeTags:   []string{"notthistag"},
					},
				},
			},
		},
		{
			name:           "EmptyFile",
			fileName:       "empty.yaml",
			fsContent:      map[string]string{"empty.yaml": ""},
			expectedResult: DefaultConfig(),
		},
	}

	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			fs := memfs.New()
			for name, content := range tt.fsContent {
				f, _ := fs.Create(name)
				_, _ = f.Write([]byte(content))
				require.NoError(t, f.Close())
			}

			cfg, err := ParseConfigFileFromFS(fs, tt.fileName)
			if tt.expectError {
				require.Error(t, err)
			} else {
				require.NoError(t, err)
				require.Equal(t, tt.expectedResult.Platform, cfg.Platform)
				if cfg.GHActions.Exclude != nil {
					require.Equal(t, tt.expectedResult.GHActions.Exclude, cfg.GHActions.Exclude)
				}
				if cfg.Images.ExcludeImages != nil {
					require.Equal(t, tt.expectedResult.Images.ExcludeImages, cfg.Images.ExcludeImages)
				}
				if cfg.Images.ExcludeTags != nil {
					require.Equal(t, tt.expectedResult.Images.ExcludeTags, cfg.Images.ExcludeTags)
				}
				if cfg.GHActions.ExcludeBranches != nil {
					require.Equal(t, tt.expectedResult.GHActions.ExcludeBranches, cfg.GHActions.ExcludeBranches)
				}
			}
		})
	}
}
0707010000002D000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001F00000000frizbee-0.1.7/pkg/utils/ghrest0707010000002E000081A400000000000000000000000167E26F5900000844000000000000000000000000000000000000002900000000frizbee-0.1.7/pkg/utils/ghrest/ghrest.go//
// Copyright 2024 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package ghrest provides a wrapper around the go-github client that implements the internal REST API
package ghrest

import (
	"bytes"
	"context"
	"io"
	"net/http"

	"github.com/google/go-github/v66/github"
)

// Client is the struct that contains the GitHub REST API client
// this struct implements the REST API
type Client struct {
	client *github.Client
}

// NewClient creates a new instance of GhRest
func NewClient(token string) *Client {
	ghcli := github.NewClient(nil)

	if token != "" {
		ghcli = ghcli.WithAuthToken(token)
	}
	return &Client{
		client: ghcli,
	}
}

// NewRequest creates an API request. A relative URL can be provided in urlStr,
// which will be resolved to the BaseURL of the Client. Relative URLS should
// always be specified without a preceding slash. If specified, the value
// pointed to by body is JSON encoded and included as the request body.
func (c *Client) NewRequest(method, requestUrl string, body any) (*http.Request, error) {
	return c.client.NewRequest(method, requestUrl, body)
}

// Do sends an API request and returns the API response.
func (c *Client) Do(ctx context.Context, req *http.Request) (*http.Response, error) {
	var buf bytes.Buffer

	// The GitHub client closes the response body, so we need to capture it
	// in a buffer so that we can return it to the caller
	resp, err := c.client.Do(ctx, req, &buf)
	if err != nil && resp == nil {
		return nil, err
	}

	if resp.Response != nil {
		resp.Response.Body = io.NopCloser(&buf)
	}

	return resp.Response, err
}
0707010000002F000081A400000000000000000000000167E26F5900000AC7000000000000000000000000000000000000002E00000000frizbee-0.1.7/pkg/utils/ghrest/ghrest_test.gopackage ghrest

import (
	"context"
	"errors"
	"io"
	"net/http"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gopkg.in/h2non/gock.v1"
)

// nolint:gocyclo
func TestClientFunctions(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name           string
		token          string
		method         string
		url            string
		mockResponse   *gock.Response
		expectedMethod string
		expectedURL    string
		expectError    bool
		expectedStatus int
		expectedBody   string
	}{
		{
			name:           "NewClient",
			token:          "test_token",
			expectedMethod: "",
			expectedURL:    "",
		},
		{
			name:           "NewRequest GET",
			token:          "",
			method:         "GET",
			url:            "test_url",
			expectedMethod: http.MethodGet,
			expectedURL:    "https://api.github.com/test_url",
		},
		{
			name:           "Do successful request",
			token:          "",
			method:         "GET",
			url:            "test",
			mockResponse:   gock.New("https://api.github.com").Get("/test").Reply(200).BodyString(`{"message": "hello world"}`),
			expectedMethod: http.MethodGet,
			expectedURL:    "https://api.github.com/test",
			expectedStatus: http.StatusOK,
			expectedBody:   `{"message": "hello world"}`,
		},
		{
			name:           "Do failed request",
			token:          "",
			method:         "GET",
			url:            "test",
			mockResponse:   gock.New("https://api.github.com").Get("/test").ReplyError(errors.New("failed request")),
			expectedMethod: http.MethodGet,
			expectedURL:    "https://api.github.com/test",
			expectError:    true,
		},
	}

	for _, tt := range testCases {
		if tt.mockResponse != nil {
			defer gock.Off()
			//gock.DisableNetworking()
			//t.Logf("Mock response configured for %s %s", tt.method, tt.url)
		}

		client := NewClient(tt.token)

		if tt.name == "NewClient" {
			assert.NotNil(t, client, "NewClient returned nil")
			assert.NotNil(t, client.client, "NewClient returned client with nil GitHub client")
			return
		}

		req, err := client.NewRequest(tt.method, tt.url, nil)
		require.NoError(t, err)
		require.Equal(t, req.Method, tt.expectedMethod)
		require.Equal(t, req.URL.String(), tt.expectedURL)

		if tt.name == "NewRequest GET" {
			return
		}

		ctx := context.Background()

		resp, err := client.Do(ctx, req)
		if tt.expectError {
			require.NotNil(t, err, "Expected error, got nil")
			require.Nil(t, resp, "Expected nil response, got %v", resp)
			return
		}
		require.Nil(t, err, "Expected no error, got %v", err)

		require.Equal(t, resp.StatusCode, tt.expectedStatus)

		body, err := io.ReadAll(resp.Body)
		require.Nil(t, err)
		require.Equal(t, string(body), tt.expectedBody)
		defer resp.Body.Close() // nolint:errcheck
	}
}
07070100000030000041ED00000000000000000000000267E26F5900000000000000000000000000000000000000000000001E00000000frizbee-0.1.7/pkg/utils/store07070100000031000081A400000000000000000000000167E26F5900000702000000000000000000000000000000000000002700000000frizbee-0.1.7/pkg/utils/store/cache.go//
// Copyright 2024 Stacklok, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package store provides utilities to work with a cache store.
package store

import (
	"github.com/puzpuzpuz/xsync"
)

// RefCacher is an interface for caching references.
type RefCacher interface {
	Store(key, value string)
	Load(key string) (string, bool)
}

type refCacher struct {
	cache *xsync.MapOf[string, string]
}

// NewRefCacher returns a new RefCacher. The default implementation is
// thread-safe.
func NewRefCacher() RefCacher {
	return &refCacher{
		cache: xsync.NewMapOf[string](),
	}
}

// Store stores a key-value pair.
func (r *refCacher) Store(key, value string) {
	r.cache.Store(key, value)
}

// Load loads a value for a given key.
func (r *refCacher) Load(key string) (string, bool) {
	return r.cache.Load(key)
}

type unsafeCacher struct {
	cache map[string]string
}

// NewUnsafeCacher returns a new RefCacher that's not thread-safe.
func NewUnsafeCacher() RefCacher {
	return &unsafeCacher{
		cache: map[string]string{},
	}
}

// Store stores a key-value pair.
func (r *unsafeCacher) Store(key, value string) {
	r.cache[key] = value
}

// Load loads a value for a given key.
func (r *unsafeCacher) Load(key string) (string, bool) {
	v, ok := r.cache[key]
	return v, ok
}
07070100000032000081A400000000000000000000000167E26F59000009B2000000000000000000000000000000000000002C00000000frizbee-0.1.7/pkg/utils/store/cache_test.gopackage store

import (
	"fmt"
	"testing"

	"github.com/stretchr/testify/require"
)

type Cacher interface {
	Store(key, value string)
	Load(key string) (string, bool)
}

// TestCacher tests the creation and basic functionality of both refCacher and unsafeCacher.
func TestCacher(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name        string
		cacher      Cacher
		key         string
		storeValue  string
		loadKey     string
		expectedVal string
		expectFound bool
	}{
		{
			name:        "RefCacher store and load existing key",
			cacher:      NewRefCacher(),
			key:         "key1",
			storeValue:  "value1",
			loadKey:     "key1",
			expectedVal: "value1",
			expectFound: true,
		},
		{
			name:        "RefCacher load non-existing key",
			cacher:      NewRefCacher(),
			key:         "key1",
			storeValue:  "value1",
			loadKey:     "key2",
			expectedVal: "",
			expectFound: false,
		},
		{
			name:        "UnsafeCacher store and load existing key",
			cacher:      NewUnsafeCacher(),
			key:         "key1",
			storeValue:  "value1",
			loadKey:     "key1",
			expectedVal: "value1",
			expectFound: true,
		},
		{
			name:        "UnsafeCacher load non-existing key",
			cacher:      NewUnsafeCacher(),
			key:         "key1",
			storeValue:  "value1",
			loadKey:     "key2",
			expectedVal: "",
			expectFound: false,
		},
	}

	for _, tt := range testCases {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			tt.cacher.Store(tt.key, tt.storeValue)
			val, ok := tt.cacher.Load(tt.loadKey)
			require.Equal(t, tt.expectFound, ok)
			require.Equal(t, tt.expectedVal, val)
		})
	}
}

// TestConcurrency tests the thread-safety of refCacher.
func TestConcurrency(t *testing.T) {
	t.Parallel()

	cacher := NewRefCacher()
	iterations := 1000
	done := make(chan bool)

	// Concurrently store values
	for i := 0; i < iterations; i++ {
		go func(i int) {
			key := fmt.Sprintf("key%d", i)
			value := fmt.Sprintf("value%d", i)
			cacher.Store(key, value)
			done <- true
		}(i)
	}

	// Wait for all goroutines to finish storing
	for i := 0; i < iterations; i++ {
		<-done
	}

	// Concurrently load values
	for i := 0; i < iterations; i++ {
		go func(i int) {
			key := fmt.Sprintf("key%d", i)
			val, ok := cacher.Load(key)
			expectedVal := fmt.Sprintf("value%d", i)
			require.True(t, ok)
			require.Equal(t, expectedVal, val)
			done <- true
		}(i)
	}

	// Wait for all goroutines to finish loading
	for i := 0; i < iterations; i++ {
		<-done
	}
}
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!372 blocks
openSUSE Build Service is sponsored by