Compare commits

...

No commits in common. "v0.0.1" and "main" have entirely different histories.
v0.0.1 ... main

274 changed files with 29295 additions and 4089 deletions

20
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@ -0,0 +1,20 @@
---
name: Bug Report
about: Report a bug encountered while using the falcoctl
labels: kind/bug
---
<!-- Please use this template while reporting a bug and provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner. Thanks!
If the matter is security related, please disclose it privately via https://falco.org/security/
-->
**What happened**:
**What you expected to happen**:
**How to reproduce it (as minimally and precisely as possible)**:
**Anything else we need to know?**:

11
.github/ISSUE_TEMPLATE/enhancement.md vendored Normal file
View File

@ -0,0 +1,11 @@
---
name: Enhancement Request
about: Suggest an enhancement to the Falco project
labels: kind/feature
---
<!-- Please only use this template for submitting enhancement requests -->
**What would you like to be added**:
**Why is this needed**:

20
.github/ISSUE_TEMPLATE/failing-tests.md vendored Normal file
View File

@ -0,0 +1,20 @@
---
name: Failing Test
about: Report test failures in Falco CI jobs
labels: kind/failing-test
---
<!-- Please only use this template for submitting reports about failing tests in Falco CI jobs -->
**Which jobs are failing**:
**Which test(s) are failing**:
**Since when has it been failing**:
**Test link**:
**Reason for failure**:
**Anything else we need to know**:

51
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,51 @@
<!-- Thanks for sending a pull request! Here are some tips for you:
1. If this is your first time, please read our contributor guidelines in the [CONTRIBUTING.md](https://github.com/falcosecurity/.github/blob/main/CONTRIBUTING.md) file in the Falco `.github` repository.
2. Please label this pull request according to what type of issue you are addressing.
3. Please add a release note!
4. If the PR is unfinished while opening it specify a wip in the title before the actual title, for example, "wip: my awesome feature"
-->
**What type of PR is this?**
> Uncomment one (or more) `/kind <>` lines:
> /kind bug
> /kind cleanup
> /kind design
> /kind documentation
> /kind failing-test
> /kind feature
> /kind flaky-test
**Any specific area of the project related to this PR?**
> Uncomment one (or more) `/area <>` lines:
> /area library
> /area cli
> /area tests
> /area examples
**What this PR does / why we need it**:
**Which issue(s) this PR fixes**:
<!--
Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
If PR is `kind/failing-tests` or `kind/flaky-test`, please post the related issues/tests in a comment and do not use `Fixes`.
-->
Fixes #
**Special notes for your reviewer**:

22
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,22 @@
version: 2
updates:
- package-ecosystem: gomod
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
groups:
gomod:
update-types:
- "patch"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
groups:
actions:
update-types:
- "minor"
- "patch"

37
.github/workflows/codeql-analysis.yaml vendored Normal file
View File

@ -0,0 +1,37 @@
name: "CodeQL"
on:
push:
branches:
- main
pull_request:
branches:
- main
schedule:
- cron: '28 11 * * 2'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language:
- go
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@d23060145bc9131d50558d5d4185494a20208101 # v2.2.8
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@d23060145bc9131d50558d5d4185494a20208101 # v2.2.8
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@d23060145bc9131d50558d5d4185494a20208101 # v2.2.8

103
.github/workflows/docker-image.yaml vendored Normal file
View File

@ -0,0 +1,103 @@
name: docker-image
on:
workflow_call:
inputs:
release:
required: true
type: string
commit:
required: true
type: string
build_date:
required: true
type: string
sign:
required: false
default: false
type: boolean
outputs:
digest:
description: The digest of the pushed image.
value: ${{ jobs.docker-image.outputs.digest }}
permissions:
contents: read
id-token: write
jobs:
docker-image:
runs-on: ubuntu-22.04
outputs:
image: ${{ steps.build-and-push.outputs.image }}
digest: ${{ steps.build-and-push.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Set up Docker Buildx
id: Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Login to Docker Hub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_SECRET }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
with:
role-to-assume: arn:aws:iam::292999226676:role/github_actions-falcoctl-ecr
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr-public
uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1
with:
registry-type: public
- name: Docker Meta
id: meta_falcoctl
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
# list of Docker images to use as base name for tags
images: |
docker.io/falcosecurity/falcoctl
public.ecr.aws/falcosecurity/falcoctl
tags: |
type=ref,event=branch
type=semver,pattern={{ version }}
type=semver,pattern={{ major }}
type=semver,pattern={{ major }}.{{ minor }}
- name: Build and push
id: build-and-push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta_falcoctl.outputs.tags }}
file: ./build/Dockerfile
build-args: |
RELEASE=${{ inputs.release }}
COMMIT=${{ inputs.commit }}
BUILD_DATE=${{ inputs.build_date }}
- name: Install Cosign
if: ${{ inputs.sign }}
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
- name: Sign the images with GitHub OIDC Token
if: ${{ inputs.sign }}
env:
DIGEST: ${{ steps.build-and-push.outputs.digest }}
TAGS: ${{ steps.meta_falcoctl.outputs.tags }}
COSIGN_YES: "true"
run: echo "${TAGS}" | xargs -I {} cosign sign {}@${DIGEST}

166
.github/workflows/integration.yaml vendored Normal file
View File

@ -0,0 +1,166 @@
name: Integration Pipeline
on:
push:
branches:
- main
pull_request:
jobs:
build:
runs-on: ubuntu-22.04
strategy:
matrix:
goos:
- linux
- darwin
- windows
goarch:
- arm64
- amd64
exclude:
- goarch: arm64
goos: windows
steps:
- name: Checkout commit
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version-file: 'go.mod'
check-latest: true
- name: Build Falcoctl
run: >
go build -ldflags="-s -w" -o falcoctl-${{ matrix.goos }}-${{ matrix.goarch }} .
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
- name: Create Archives
run: |
cp falcoctl-${{ matrix.goos }}-${{ matrix.goarch }} falcoctl
tar -czvf falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz falcoctl LICENSE
- name: Upload falcoctl artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}
path: ./falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}
retention-days: 1
- name: Upload falcoctl archives
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
path: ./falcoctl-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
retention-days: 1
docker-configure:
if: ${{ github.event_name == 'push' }}
runs-on: ubuntu-22.04
outputs:
release: ${{ steps.vars.outputs.release }}
commit: ${{ steps.vars.outputs.commit }}
build_date: ${{ steps.vars.outputs.build_date }}
steps:
- name: Set version fields
id: vars
run: |
echo "release=${{ github.sha }}" >> $GITHUB_OUTPUT
echo "commit=${{ github.sha }}" >> $GITHUB_OUTPUT
echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
docker-image:
if: ${{ github.event_name == 'push' }}
needs: docker-configure
uses: ./.github/workflows/docker-image.yaml
secrets: inherit
permissions:
contents: read
id-token: write
with:
release: ${{ needs.docker-configure.outputs.release }}
commit: ${{ needs.docker-configure.outputs.commit }}
build_date: ${{ needs.docker-configure.outputs.build_date }}
sign: true
provenance-for-images-docker:
if: ${{ github.event_name == 'push' }}
needs: [docker-configure, docker-image]
permissions:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: docker.io/falcosecurity/falcoctl
# The image digest is used to prevent TOCTOU issues.
# This is an output of the docker/build-push-action
# See: https://github.com/slsa-framework/slsa-verifier#toctou-attacks
digest: ${{ needs.docker-image.outputs.digest }}
secrets:
registry-username: ${{ secrets.DOCKERHUB_USER }}
registry-password: ${{ secrets.DOCKERHUB_SECRET }}
login-to-amazon-ecr:
if: ${{ github.event_name == 'push' }}
runs-on: ubuntu-22.04
permissions:
contents: read
id-token: write
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
with:
role-to-assume: arn:aws:iam::292999226676:role/github_actions-falcoctl-ecr
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr-public
uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1
with:
registry-type: public
mask-password: 'false'
outputs:
registry: ${{ steps.login-ecr-public.outputs.registry }}
docker_username: ${{ steps.login-ecr-public.outputs.docker_username_public_ecr_aws }}
docker_password: ${{ steps.login-ecr-public.outputs.docker_password_public_ecr_aws }}
provenance-for-images-aws-ecr:
if: ${{ github.event_name == 'push' }}
needs: [docker-configure, docker-image, login-to-amazon-ecr]
permissions:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: public.ecr.aws/falcosecurity/falcoctl
# The image digest is used to prevent TOCTOU issues.
# This is an output of the docker/build-push-action
# See: https://github.com/slsa-framework/slsa-verifier#toctou-attacks
digest: ${{ needs.docker-image.outputs.digest }}
secrets:
registry-username: ${{ needs.login-to-amazon-ecr.outputs.docker_username }}
registry-password: ${{ needs.login-to-amazon-ecr.outputs.docker_password }}
test:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout commit
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version-file: 'go.mod'
check-latest: true
- name: Run tests
run: go test -cover ./...

64
.github/workflows/lint.yaml vendored Normal file
View File

@ -0,0 +1,64 @@
name: Linting
on:
pull_request:
jobs:
golangci:
name: Lint golang files
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{github.event.pull_request.head.repo.full_name}}
persist-credentials: false
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: "^1.24.3"
go-version-file: "go.mod"
check-latest: true
cache: "false"
- name: golangci-lint
uses: golangci/golangci-lint-action@55c2c1448f86e01eaae002a5a3a9624417608d84 # v6.5.2
with:
only-new-issues: true
version: v1.64.7
args: --timeout=900s
gomodtidy:
name: Enforce go.mod tidiness
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: "${{ github.event.pull_request.head.sha }}"
repository: ${{github.event.pull_request.head.repo.full_name}}
persist-credentials: false
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version-file: "go.mod"
check-latest: true
- name: Execute go mod tidy and check the outcome
working-directory: ./
run: |
go mod tidy
exit_code=$(git diff --exit-code)
exit ${exit_code}
- name: Print a comment in case of failure
run: |
echo "The go.mod and/or go.sum files appear not to be correctly tidied.
Please, rerun go mod tidy to fix the issues."
exit 1
if: |
failure() && github.event.pull_request.head.repo.full_name == github.repository

178
.github/workflows/release.yaml vendored Normal file
View File

@ -0,0 +1,178 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
goreleaser:
runs-on: ubuntu-22.04
permissions:
contents: write # To add assets to a release.
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Fetch all tags
run: git fetch --force --tags
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version-file: 'go.mod'
check-latest: true
- name: Run GoReleaser
id: run-goreleaser
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate subject
id: hash
env:
ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}"
run: |
set -euo pipefail
checksum_file=$(echo "$ARTIFACTS" | jq -r '.[] | select (.type=="Checksum") | .path')
echo "hashes=$(cat $checksum_file | base64 -w0)" >> "$GITHUB_OUTPUT"
provenance-for-binaries:
needs: [goreleaser]
permissions:
actions: read # To read the workflow path.
id-token: write # To sign the provenance.
contents: write # To add assets to a release.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
base64-subjects: "${{ needs.goreleaser.outputs.hashes }}"
upload-assets: true # upload to a new release
verification:
needs: [goreleaser, provenance-for-binaries]
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Install the verifier
uses: slsa-framework/slsa-verifier/actions/installer@v2.7.1
- name: Download assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PROVENANCE: "${{ needs.provenance-for-binaries.outputs.provenance-name }}"
run: |
set -euo pipefail
gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "*.tar.gz"
gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "*.zip"
gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "$PROVENANCE"
- name: Verify assets
env:
CHECKSUMS: ${{ needs.goreleaser.outputs.hashes }}
PROVENANCE: "${{ needs.provenance-for-binaries.outputs.provenance-name }}"
run: |
set -euo pipefail
checksums=$(echo "$CHECKSUMS" | base64 -d)
while read -r line; do
fn=$(echo $line | cut -d ' ' -f2)
echo "Verifying $fn"
slsa-verifier verify-artifact --provenance-path "$PROVENANCE" \
--source-uri "github.com/$GITHUB_REPOSITORY" \
--source-tag "$GITHUB_REF_NAME" \
"$fn"
done <<<"$checksums"
docker-configure:
runs-on: ubuntu-22.04
outputs:
release: ${{ steps.vars.outputs.release }}
commit: ${{ steps.vars.outputs.commit }}
build_date: ${{ steps.vars.outputs.build_date }}
steps:
- name: Set version fields
id: vars
run: |
echo "release=$(echo $GITHUB_REF | cut -d / -f 3 | sed 's/^v//')" >> $GITHUB_OUTPUT
echo "commit=${{ github.sha }}" >> $GITHUB_OUTPUT
echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
docker-image:
needs: docker-configure
uses: ./.github/workflows/docker-image.yaml
secrets: inherit
permissions:
contents: read
packages: write
id-token: write # needed for signing the images with GitHub OIDC Token
with:
release: ${{ needs.docker-configure.outputs.release }}
commit: ${{ needs.docker-configure.outputs.commit }}
build_date: ${{ needs.docker-configure.outputs.build_date }}
sign: true
provenance-for-images-docker:
needs: [docker-configure, docker-image]
permissions:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: docker.io/falcosecurity/falcoctl
# The image digest is used to prevent TOCTOU issues.
# This is an output of the docker/build-push-action
# See: https://github.com/slsa-framework/slsa-verifier#toctou-attacks
digest: ${{ needs.docker-image.outputs.digest }}
secrets:
registry-username: ${{ secrets.DOCKERHUB_USER }}
registry-password: ${{ secrets.DOCKERHUB_SECRET }}
login-to-amazon-ecr:
runs-on: ubuntu-22.04
permissions:
contents: read
id-token: write
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
with:
role-to-assume: arn:aws:iam::292999226676:role/github_actions-falcoctl-ecr
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr-public
uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1
with:
registry-type: public
mask-password: 'false'
outputs:
registry: ${{ steps.login-ecr-public.outputs.registry }}
docker_username: ${{ steps.login-ecr-public.outputs.docker_username_public_ecr_aws }}
docker_password: ${{ steps.login-ecr-public.outputs.docker_password_public_ecr_aws }}
provenance-for-images-aws-ecr:
needs: [docker-configure, docker-image, login-to-amazon-ecr]
permissions:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: public.ecr.aws/falcosecurity/falcoctl
# The image digest is used to prevent TOCTOU issues.
# This is an output of the docker/build-push-action
# See: https://github.com/slsa-framework/slsa-verifier#toctou-attacks
digest: ${{ needs.docker-image.outputs.digest }}
secrets:
registry-username: ${{ needs.login-to-amazon-ecr.outputs.docker_username }}
registry-password: ${{ needs.login-to-amazon-ecr.outputs.docker_password }}

5
.gitignore vendored
View File

@ -1,3 +1,6 @@
*.idea *.idea
*.idea* *.idea*
.idea/* .idea/*
.vscode/*
falcoctl
dist/

139
.golangci.yml Normal file
View File

@ -0,0 +1,139 @@
run:
timeout: 10m
linters-settings:
exhaustive:
check-generated: false
default-signifies-exhaustive: true
goheader:
values:
const:
AUTHORS: The Falco Authors
template: |-
SPDX-License-Identifier: Apache-2.0
Copyright (C) {{ YEAR }} {{ AUTHORS }}
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.
lll:
line-length: 150
gci:
sections:
- standard # Captures all standard packages if they do not match another section.
- default # Contains all imports that could not be matched to another section type.
- prefix(github.com/falcosecurity/falcoctl) # Groups all imports with the specified Prefix.
goconst:
min-len: 2
min-occurrences: 2
gocritic:
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
goimports:
local-prefixes: github.com/falcosecurity/falcoctl
misspell:
locale: US
nolintlint:
allow-unused: false # report any unused nolint directives
require-explanation: true # require an explanation for nolint directives
require-specific: true # require nolint directives to be specific about which linter is being skipped
dupl:
threshold: 300
linters:
disable-all: true
enable:
- asciicheck
- bodyclose
- dogsled
- dupl
- errcheck
- errorlint
- exhaustive
- copyloopvar
# - funlen
# - gochecknoglobals
# - gochecknoinits
# - gocognit
- gci
- goconst
- gocritic
- gocyclo
- godot
# - godox
# - goerr113
- gofmt
- goheader
- goimports
- gomodguard
# - gomnd
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- lll
# - maligned
- misspell
- nakedret
# - nestif
- noctx
- nolintlint
# - prealloc
- revive
- rowserrcheck
- staticcheck
- stylecheck
# - testpackage
- typecheck
- unconvert
- unparam
- unused
- whitespace
# - wsl
issues:
#fix: true
max-issues-per-linter: 0
max-same-issues: 0
# Disable the default exclude patterns (as they disable the mandatory comments)
exclude-use-default: false
exclude:
# errcheck: Almost all programs ignore errors on these functions and in most cases it's ok
- Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked
exclude-rules:
- linters:
- govet
text: 'declaration of "(err|ctx)" shadows declaration at'
- linters:
- errorlint
# Disable the check to test errors type assertion on switches.
text: type switch on error will fail on wrapped errors. Use errors.As to check for specific errors
# Disable goheader for multi authors files
- linters:
- goheader
path: pkg/oci/authn/credentialstore.go
# Exclude the following linters from running on tests files.
- path: _test\.go
linters:
- gosec

52
.goreleaser.yml Normal file
View File

@ -0,0 +1,52 @@
version: 2
project_name: falcoctl
before:
hooks:
- go mod tidy
builds:
- id: "falcoctl"
goos:
- linux
- darwin
- windows
goarch:
- amd64
- 386
- arm64
ignore:
- goos: darwin
goarch: 386
- goos: windows
goarch: 386
ldflags: |
-X github.com/falcosecurity/falcoctl/cmd/version.buildDate={{ .Date }}
-X github.com/falcosecurity/falcoctl/cmd/version.gitCommit={{ .Commit }}
-X github.com/falcosecurity/falcoctl/cmd/version.semVersion={{ if .IsSnapshot }}{{ .Commit }}{{ else }}{{ .Version }}{{ end }}
-s
-w
main: .
env:
- GO111MODULE=on
- CGO_ENABLED=0
archives:
- id: windows
format_overrides:
- goos: windows
format: zip
snapshot:
name_template: "{{ .ShortCommit }}"
release:
prerelease: auto
mode: replace
changelog:
use: github-native
git:
tag_sort: -version:creatordate

191
LICENSE Normal file
View File

@ -0,0 +1,191 @@
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
Copyright 2019 The Falco Authors
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.

84
Makefile Normal file
View File

@ -0,0 +1,84 @@
SHELL=/bin/bash -o pipefail
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif
GO ?= go
DOCKER ?= docker
# version settings
RELEASE?=$(shell git rev-parse HEAD)
COMMIT?=$(shell git rev-parse HEAD)
BUILD_DATE?=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
PROJECT?=github.com/falcosecurity/falcoctl
# todo(leogr): re-enable race when CLI tests can run with race enabled
TEST_FLAGS ?= -v -cover# -race
.PHONY: falcoctl
falcoctl:
$(GO) build -ldflags \
"-X '${PROJECT}/cmd/version.semVersion=${RELEASE}' \
-X '${PROJECT}/cmd/version.gitCommit=${COMMIT}' \
-X '${PROJECT}/cmd/version.buildDate=${BUILD_DATE}'" \
-o falcoctl .
.PHONY: test
test:
$(GO) vet ./...
$(GO) test ${TEST_FLAGS} ./...
# Install gci if not available
.PHONY: gci
gci:
ifeq (, $(shell which gci))
@go install github.com/daixiang0/gci@v0.11.1
GCI=$(GOBIN)/gci
else
GCI=$(shell which gci)
endif
# Install addlicense if not available
.PHONY: addlicense
addlicense:
ifeq (, $(shell which addlicense))
@go install github.com/google/addlicense@v1.0.0
ADDLICENSE=$(GOBIN)/addlicense
else
ADDLICENSE=$(shell which addlicense)
endif
# Run go fmt against code and add the licence header
.PHONY: fmt
fmt: gci addlicense
go mod tidy
go fmt ./...
find . -type f -name '*.go' -a -exec $(GCI) write -s standard -s default -s "prefix(github.com/falcosecurity/falcoctl)" {} \;
find . -type f -name '*.go' -exec $(ADDLICENSE) -l apache -s -c "The Falco Authors" -y "$(shell date +%Y)" {} \;
# Install golangci-lint if not available
.PHONY: golangci-lint
golangci-lint:
ifeq (, $(shell which golangci-lint))
@go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2
GOLANGCILINT=$(GOBIN)/golangci-lint
else
GOLANGCILINT=$(shell which golangci-lint)
endif
# It works when called in a branch different than main.
# "--new-from-rev REV Show only new issues created after git revision REV"
.PHONY: lint
lint: golangci-lint
$(GOLANGCILINT) run --new-from-rev main
.PHONY: docker
docker:
$(DOCKER) build -f ./build/Dockerfile . --build-arg RELEASE=${RELEASE} --build-arg COMMIT=${COMMIT} --build-arg BUILD_DATE=${BUILD_DATE}
.PHONY: clean
clean:
@rm falcoctl || true

14
OWNERS Normal file
View File

@ -0,0 +1,14 @@
approvers:
- leogr
- zuc
- maxgio92
- fededp
- cpanato
- alacuku
- loresuso
emeritus_approvers:
- kris-nova
- markyjackson-taulia
- leodido
- fntlnz
- mstemm

404
README.md Normal file
View File

@ -0,0 +1,404 @@
# 🧰 falcoctl
[![Falco Core Repository](https://github.com/falcosecurity/evolution/blob/main/repos/badges/falco-core-blue.svg)](https://github.com/falcosecurity/evolution/blob/main/REPOSITORIES.md#core-scope) [![Stable](https://img.shields.io/badge/status-stable-brightgreen?style=for-the-badge)](https://github.com/falcosecurity/evolution/blob/main/REPOSITORIES.md#stable) [![License](https://img.shields.io/github/license/falcosecurity/falcoctl?style=for-the-badge)](./LICENSE)
The official CLI tool for working with [Falco](https://github.com/falcosecurity/falco) and its [ecosystem components](https://falco.org/docs/#what-are-the-ecosystem-projects-that-can-interact-with-falco).
## Installation
### Install falcoctl manually
You can download and install *falcoctl* manually following the appropriate instructions based on your operating system architecture.
#### Linux
##### AMD64
```bash
LATEST=$(curl -sI https://github.com/falcosecurity/falcoctl/releases/latest | awk '/location: /{gsub("\r","",$2);split($2,v,"/");print substr(v[8],2)}')
curl --fail -LS "https://github.com/falcosecurity/falcoctl/releases/download/v${LATEST}/falcoctl_${LATEST}_linux_amd64.tar.gz" | tar -xz
sudo install -o root -g root -m 0755 falcoctl /usr/local/bin/falcoctl
```
##### ARM64
```bash
LATEST=$(curl -sI https://github.com/falcosecurity/falcoctl/releases/latest | awk '/location: /{gsub("\r","",$2);split($2,v,"/");print substr(v[8],2)}')
curl --fail -LS "https://github.com/falcosecurity/falcoctl/releases/download/v${LATEST}/falcoctl_${LATEST}_linux_arm64.tar.gz" | tar -xz
sudo install -o root -g root -m 0755 falcoctl /usr/local/bin/falcoctl
```
> NOTE: Make sure */usr/local/bin* is in your PATH environment variable.
#### MacOS
The easiest way to install on MacOS is via `Homebrew`:
```bash
brew install falcoctl
```
Alternatively, you can download directly from the source:
##### Intel
```bash
LATEST=$(curl -sI https://github.com/falcosecurity/falcoctl/releases/latest | awk '/location: /{gsub("\r","",$2);split($2,v,"/");print substr(v[8],2)}')
curl --fail -LS "https://github.com/falcosecurity/falcoctl/releases/download/v${LATEST}/falcoctl_${LATEST}_darwin_amd64.tar.gz" | tar -xz
chmod +x falcoctl
sudo mv falcoctl /usr/local/bin/falcoctl
```
##### Apple Silicon
```bash
LATEST=$(curl -sI https://github.com/falcosecurity/falcoctl/releases/latest | awk '/location: /{gsub("\r","",$2);split($2,v,"/");print substr(v[8],2)}')
curl --fail -LS "https://github.com/falcosecurity/falcoctl/releases/download/v${LATEST}/falcoctl_${LATEST}_darwin_arm64.tar.gz" | tar -xz
chmod +x falcoctl
sudo mv falcoctl /usr/local/bin/falcoctl
```
Alternatively, you can manually download *falcoctl* from the [falcoctl releases](https://github.com/falcosecurity/falcoctl/releases) page on GitHub.
### Install falcoctl from source
You can install *falcoctl* from source. First thing clone the *falcoctl* repository, build the *falcoctl* binary, and move it to a file location in your system **PATH**.
```bash
git clone https://github.com/falcosecurity/falcoctl.git
cd falcoctl
make falcoctl
sudo mv falcoctl /usr/local/bin/falcoctl
```
# Getting Started
## Installing an artifact
This tutorial aims at presenting how to install a Falco artifact. The next few steps will present us with the fundamental commands of *falcoctl* and how to use them.
First thing, we need to add a new `index` to *falcoctl*:
```bash
$ falcoctl index add falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml
```
We just downloaded the metadata of the **artifacts** hosted and distributed by the **falcosecurity** organization and made them available to the *falcoctl* tool.
Now let's check that the `index` file is in place by running:
```
$ falcoctl index list
```
We should get an output similar to this one:
```
NAME URL ADDED UPDATED
falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml 2022-10-25 15:01:25 2022-10-25 15:01:25
```
Now let's search all the artifacts related to *cloudtrail*:
```
$ falcoctl artifact search cloudtrail
INDEX ARTIFACT TYPE REGISTRY REPOSITORY
falcosecurity cloudtrail plugin ghcr.io falcosecurity/plugins/plugin/cloudtrail
falcosecurity cloudtrail-rules rulesfile ghcr.io falcosecurity/plugins/ruleset/cloudtrail
```
Lets install the *cloudtrail plugin*:
```
$ falcoctl artifact install cloudtrail --plugins-dir=./
INFO Reading all configured index files from "/home/aldo/.config/falcoctl/indexes.yaml"
INFO Preparing to pull "ghcr.io/falcosecurity/plugins/plugin/cloudtrail:latest"
INFO Remote registry "ghcr.io" implements docker registry API V2
INFO Pulling 44136fa355b3: ############################################# 100%
INFO Pulling 80e0c33f30c0: ############################################# 100%
INFO Pulling b024dd7a2a63: ############################################# 100%
INFO Artifact successfully installed in "./"
```
Install the *cloudtrail-rules* rulesfile:
```
$ ./falcoctl artifact install cloudtrail-rules --rulesfiles-dir=./
INFO Reading all configured index files from "/home/aldo/.config/falcoctl/indexes.yaml"
INFO Preparing to pull "ghcr.io/falcosecurity/plugins/ruleset/cloudtrail:latest"
INFO Remote registry "ghcr.io" implements docker registry API V2
INFO Pulling 44136fa355b3: ############################################# 100%
INFO Pulling e0dccb7b0f1d: ############################################# 100%
INFO Pulling 575bced78731: ############################################# 100%
INFO Artifact successfully installed in "./"
```
We should have now two new files in the current directory: `aws_cloudtrail_rules.yaml` and `libcloudtrail.so`.
# Falcoctl Configuration Files
## `/etc/falcoctl/falcoctl.yaml`
The `falco configuration file` is a yaml file that contains some metadata about the `falcoctl` behaviour.
It contains the list of the indexes where the artifacts are listed, how often and which artifacts needed to be updated periodically.
The default configuration is stored in `/etc/falcoctl/falcoctl.yaml`.
This is an example of a falcoctl configuration file:
``` yaml
artifact:
follow:
every: 6h0m0s
falcoVersions: http://localhost:8765/versions
refs:
- falco-rules:0
- my-rules:1
install:
refs:
- cloudtrail-rules:latest
- cloudtrail:latest
rulesfilesdir: /tmp/rules
pluginsdir: /tmp/plugins
indexes:
- name: falcosecurity
url: https://falcosecurity.github.io/falcoctl/index.yaml
- name: my-index
url: https://example.com/falcoctl/index.yaml
registry:
auth:
basic:
- password: password
registry: myregistry.example.com:5000
user: user
oauth:
- registry: myregistry.example.com:5001
clientsecret: "999999"
clientid: "000000"
tokenurl: http://myregistry.example.com:9096/token
gcp:
- registry: europe-docker.pkg.dev
```
## `~/.config/falcoctl/`
The `~/.config/falcoctl/` directory contains:
- *cache objects*
- *OAuth2 client credentials*
### `~/.config/falcoctl/indexes.yaml`
This file is used for cache purposes and contains the *index refs* added by the command `falcoctl index add [name] [ref]`. The *index ref* is enriched with two timestamps to track when it was added and the last time is was updated. Once the *index ref* is added, `falcoctl` will download the real index in the `~/.config/falcoctl/indexes/` directory. Moreover, every time the index is fetched, the `updated_timestamp` is updated.
### `~/.config/falcoctl/clientcredentials.json`
The command `falcoctl registry auth oauth` will add the `clientcredentials.json` file to the `~/.config/falcoctl/` directory. That file will contain all the needed information for the OAuth2 authetication.
# Falcoctl Commands
## Falcoctl index
The `index` file is a yaml file that contains some metadata about the Falco **artifacts**. Each entry carries information such as the name, type, registry, repository and other info for the given **artifact**. Different *falcoctl* commands rely on the metadata contained in the `index` file for their operation.
This is an example of an index file:
```yaml
- name: okta
type: plugin
registry: ghcr.io
repository: falcosecurity/plugins/plugin/okta
description: Okta Log Events
home: https://github.com/falcosecurity/plugins/tree/master/plugins/okta
keywords:
- audit
- log-events
- okta
license: Apache-2.0
maintainers:
- email: cncf-falco-dev@lists.cncf.io
name: The Falco Authors
sources:
- https://github.com/falcosecurity/plugins/tree/master/plugins/okta
- name: okta-rules
type: rulesfile
registry: ghcr.io
repository: falcosecurity/plugins/ruleset/okta
description: Okta Log Events
home: https://github.com/falcosecurity/plugins/tree/master/plugins/okta
keywords:
- audit
- log-events
- okta
- okta-rules
license: Apache-2.0
maintainers:
- email: cncf-falco-dev@lists.cncf.io
name: The Falco Authors
sources:
- https://github.com/falcosecurity/plugins/tree/master/plugins/okta/rules
```
### Index Storage Backends
Indices for *falcoctl* can be retrieved from various storage backends. The supported index storage backends are listed in the table below. Note if you do not specify a backend type when adding a new index *falcoctl* will try to guess based on the `URI Scheme`:
| Name | URI Scheme | Description |
| ----- | ---------- | --------------------------------------------------------------------------------------------- |
| http | http:// | Can be used to retrieve indices via simple HTTP GET requests. |
| https | https:// | Convenience alias for the HTTP backend. |
| gcs | gs:// | For indices stored as Google Cloud Storage objects. Supports application default credentials. |
| file | file:// | For indices stored on the local file system. |
| s3 | s3:// | For indices stored as AWS S3 objects. Supports default credentials, IRSA. |
#### falcoctl index add
New indexes are configured to be used by the *falcoctl* tool by adding them through the `index add` command. There are no limits to the number of indexes that can be added to the *falcoctl* tool. When adding a new index the tool adds a new entry in a file called **indexes.yaml** and downloads the *index* file in `~/.config/falcoctl`. The same folder is used to store the **indexes.yaml** file, too.
The following command adds a new index named *falcosecurity*:
```bash
$ falcoctl index add falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml
```
The following command adds the same index *falcosecurity*, but explicitly sets the storage backend to `https`:
```bash
$ falcoctl index add falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml https
```
#### falcoctl index list
Using the `index list` command you can check the configured `indexes` in your local system:
```bash
$ falcoctl index list
NAME URL ADDED UPDATED
$ falcosecurity https://falcosecurity.github.io/falcoctl/index.yaml 2022-10-25 15:01:25 2022-10-25 15:01:25
```
#### falcoctl index update
The `index update` allows to update a previously configured `index` file by syncing the local one with the remote one:
```bash
$ falcoctl index update falcosecurity
```
#### falcoctl index remove
When we want to remove an `index` file that we configured previously, the `index remove` command is the one we need:
```bash
$ falcoctl index remove falcosecurity
```
The above command will remove the **falcosecurity** index from the local system.
## Falcoctl artifact
The *falcoctl* tool provides different commands to interact with Falco **artifacts**. It makes easy to *seach*, *install* and get *info* for the **artifacts** provided by a given `index` file. For these commands to properly work we need to configure at least an `index` file in our system as shown in the previus section.
#### Falcoctl artifact search
The `artifact search` command allows to search for **artifacts** provided by the `index` files configured in *falcoctl*. The command supports searches by name or by keywords and displays all the **artifacts** that match the search. Assuming that we have already configured the `index` provided by the `falcosecurity` organization, the following command shows all the **artifacts** that work with **Kubernetes**:
```bash
$ falcoctl artifact search kubernetes
INDEX ARTIFACT TYPE REGISTRY REPOSITORY
falcosecurity k8saudit plugin ghcr.io falcosecurity/plugins/plugin/k8saudit
falcosecurity k8saudit-rules rulesfile ghcr.io falcosecurity/plugins/ruleset/k8saudit
```
#### Falcoctl artifact info
As per the name, `artifact info` prints some info for a given **artifact**:
```bash
$ falcoctl artifact info k8saudit
REF TAGS
ghcr.io/falcosecurity/plugins/plugin/k8saudit 0.1.0 0.2.0 0.2.1 0.3.0 0.4.0-rc1 0.4.0 latest
```
It shows the OCI **reference** and **tags** for the **artifact** of interest. Thot info is usually used with other commands.
#### Falcoctl artifact install
The above commands help us to find all the necessary info for a given **artifact**. The `artifact install` command installs an **artifact**. It pulls the **artifact** from remote repository, and saves it in a given directory. The following command installs the *k8saudit* plugin in the default path:
```bash
$ falcoctl artifact install k8saudit
INFO Reading all configured index files from "/home/aldo/.config/falcoctl/indexes.yaml"
INFO Preparing to pull "ghcr.io/falcosecurity/plugins/plugin/k8saudit:latest"
INFO Remote registry "ghcr.io" implements docker registry API V2
INFO Pulling 44136fa355b3: ############################################# 100%
INFO Pulling ded0b5419f40: ############################################# 100%
INFO Pulling 107d1230f3f0: ############################################# 100%
INFO Artifact successfully installed in "/usr/share/falco/plugins"
```
By default, if we give the name of an **artifact** it will search for the **artifact** in the configured `index` files and downlaod the `latest` version. The commands accepts also the OCI **reference** of an **artifact**. In this case, it will ignore the local `index` files.
The command has two flags:
* `--plugins-dir`: directory where to install plugins. Defaults to `/usr/share/falco/plugins`;
* `--rulesfiles-dir`: directory where to install rules. Defaults to `/etc/falco`.
> If the repositories of the **artifacts** your are trying to install are not public then you need to authenticate to the remote registry.
#### Falcoctl artifact follow
The above commands allow us to keep up-to-date one or more given **artifacts**. The `artifact follow` command checks for updates on a periodic basis and then downloads and installs the latest version, as specified by the passed tags.
It pulls the **artifact** from remote repository, and saves it in a given directory. The following command installs the *github-rules* rulesfile in the default path:
```bash
$ falcoctl artifact follow github-rules
WARN falcosecurity already exists with the same configuration, skipping
INFO Reading all configured index files from "/root/.config/falcoctl/indexes.yaml"
INFO: Creating follower for "github-rules", with check every 6h0m0s
INFO Starting follower for "ghcr.io/falcosecurity/plugins/ruleset/github:latest"
INFO (ghcr.io/falcosecurity/plugins/ruleset/github:latest) found new version under tag "latest"
INFO (ghcr.io/falcosecurity/plugins/ruleset/github:latest) artifact with tag "latest" correctly installed
```
By default, if we give the name of an **artifact** it will search for the **artifact** in the configured `index` files and downlaod the `latest` version. The commands accepts also the OCI **reference** of an **artifact**. In this case, it will ignore the local `index` files.
The command can specify the directory where to install the *rulesfile* artifacts through the `--rulesfiles-dir` flag (defaults to `/etc/falco`).
> If the repositories of the **artifacts** your are trying to install are not public then you need to authenticate to the remote registry.
> Please note that only **rulesfile** artifact can be followed.
## Falcoctl registry
The `registry` commands interact with OCI registries allowing the user to authenticate, pull and push artifacts. We have tested the *falcoctl* tool with the **ghcr.io** registry, but it should work with all the registries that support the OCI artifacts.
### Falcoctl registry auth
The `registry auth` command authenticates a user to a given OCI registry.
#### Falcoctl registry auth basic
The `registry auth basic` command authenticates a user to a given OCI registry using HTTP Basic Authentication. Run the command in advance for any private registries.
#### Falcoctl registry auth oauth
The `registry auth oauth` command retrieves access and refresh tokens for OAuth2.0 client credentials flow authentication. Run the command in advance for any private registries.
#### Falcoctl registry auth gcp
The `registry auth gcp` command retrieves access tokens using [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials). In particular, it supports access token retrieval using Google Compute Engine metadata server and Workload Identity, useful to authenticate your deployed Falco workloads. Run the command in advance for Artifact Registry authentication.
Two typical use cases:
1. You are manipulating some rules or plugins and use `falcoctl` to pull or push to an Artifact Registry:
1. run `gcloud auth application-default login` to generate a JSON credential file that will be used by applications.
2. run `falcoctl registry auth gcp europe-docker.pkg.dev` for instance to use Application Default Credentials to connect to any repository hosted at `europe-docker.pkg.dev`.
2. You have a Falco instance with Falcoctl as a side car, running in a GKE cluster with Workload Identity enabled:
1. Workload Identity is correctly set up for the Falco instance (see the [documentation](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity)).
2. Add an environment variable like `FALCOCTL_REGISTRY_AUTH_GCP=europe-docker.pkg.dev` to enable GCP authentication for the `europe-docker.pkg.dev` registry.
3. The Falcoctl instance will get access tokens from the metadata server and use them to authenticate to the registry and download your rules.
### Falcoctl registry push
It pushes local files and references the artifact uniquely. The following command shows how to push a local file to a remote registry:
```bash
$ falcoctl registry push --type=plugin ghcr.io/falcosecurity/plugins/plugin/cloudtrail:0.3.0 clouddrail-0.3.0-linux-x86_64.tar.gz --platform linux/amd64
```
The type denotes the **artifact** type in this case *plugins*. The `ghcr.io/falcosecurity/plugins/plugin/cloudtrail:0.3.0` is the unique reference that points to the **artifact**.
Currently, *falcoctl* supports only two types of artifacts: **plugin** and **rulesfile**. Based on **artifact type** the commands accepts different flags:
* `--add-floating-tags`: add the floating tags for the major and minor versions
* `--annotation-source`: set annotation source for the artifact;
* `--depends-on`: set an artifact dependency (can be specified multiple times). Example: `--depends-on my-plugin:1.2.3`
* `--tag`: additional artifact tag. Can be repeated multiple time
* `--type`: type of artifact to be pushed. Allowed values: `rulesfile`, `plugin`, `asset`
### Falcoctl registry pull
Pulling **artifacts** involves specifying the reference. The type of **artifact** is not required since the tool will implicitly extract it from the OCI **artifact**:
```
$ falcoctl registry pull ghcr.io/falcosecurity/plugins/plugin/cloudtrail:0.3.0
```
# Falcoctl Environment Variables
The arguments of `falcoctl` can passed as arguments through:
- command line options
- environment variables
- configuration file
The `falcoctl` arguments can be passed through these different modalities are prioritized in the following order: command line options, environment variables, and finally the configuration file. This means that if an argument is passed through multiple modalities, the value set in the command line options will take precedence over the value set in environment variables, which will in turn take precedence over the value set in the configuration file.
This is the list of the environment variable that `falcoctl` will use:
| Name | Content |
| ----------------------------------------- | ---------------------------------------------------------------- |
| `FALCOCTL_REGISTRY_AUTH_BASIC` | `registry,username,password;registry1,username1,password1` |
| `FALCOCTL_REGISTRY_AUTH_OAUTH` | `registry,client-id,client-secret,token-url;registry1` |
| `FALCOCTL_REGISTRY_AUTH_GCP` | `registry;registry1` |
| `FALCOCTL_INDEXES` | `index-name,https://falcosecurity.github.io/falcoctl/index.yaml` |
| `FALCOCTL_ARTIFACT_FOLLOW_EVERY` | `6h0m0s` |
| `FALCOCTL_ARTIFACT_FOLLOW_CRON` | `cron-formatted-string` |
| `FALCOCTL_ARTIFACT_FOLLOW_REFS` | `ref1;ref2` |
| `FALCOCTL_ARTIFACT_FOLLOW_FALCOVERSIONS` | `falco-version-url` |
| `FALCOCTL_ARTIFACT_FOLLOW_RULESFILEDIR` | `rules-directory-path` |
| `FALCOCTL_ARTIFACT_FOLLOW_PLUGINSDIR` | `plugins-directory-path` |
| `FALCOCTL_ARTIFACT_FOLLOW_TMPDIR` | `tmp-directory-path` |
| `FALCOCTL_ARTIFACT_INSTALL_REFS` | `ref1;ref2` |
| `FALCOCTL_ARTIFACT_INSTALL_RULESFILESDIR` | `rules-directory-path` |
| `FALCOCTL_ARTIFACT_INSTALL_PLUGINSDIR` | `plugins-directory-path` |
| `FALCOCTL_ARTIFACT_NOVERIFY` | |
Please note that when passing multiple arguments via an environment variable, they must be separated by a semicolon. Moreover, multiple fields of the same argument must be separated by a comma.
Here is an example of `falcoctl` usage with environment variables:
```bash
$ export FALCOCTL_REGISTRY_AUTH_OAUTH="localhost:6000,000000,999999,http://localhost:9096/token"
$ falcoctl registry oauth
```
# Container image signature verification
Official container images for Falcoctl, starting from version 0.5.0, are signed with [cosign](https://github.com/sigstore/cosign) v2. To verify the signature run:
```bash
$ FALCOCTL_VERSION=x.y.z # e.g. 0.5.0
$ cosign verify docker.io/falcosecurity/falcoctl:$FALCOCTL_VERSION --certificate-oidc-issuer=https://token.actions.githubusercontent.com --certificate-identity-regexp=https://github.com/falcosecurity/falcoctl/ --certificate-github-workflow-ref=refs/tags/v$FALCOCTL_VERSION
```

36
build/Dockerfile Normal file
View File

@ -0,0 +1,36 @@
FROM cgr.dev/chainguard/go AS builder
WORKDIR /tmp/builder
ARG RELEASE
ARG COMMIT
ARG BUILD_DATE
ARG PROJECT=github.com/falcosecurity/falcoctl
RUN test -n "$RELEASE" || ( echo "The RELEASE argument is unset. Aborting" && false )
RUN test -n "$COMMIT" || ( echo "The COMMIT argument is unset. Aborting" && false )
RUN test -n "$BUILD_DATE" || ( echo "The BUILD_DATE argument is unset. Aborting" && false )
COPY go.mod ./go.mod
COPY go.sum ./go.sum
RUN go mod download
COPY . ./
RUN CGO_ENABLED=0 \
GOOS=$(go env GOOS) \
GOARCH=$(go env GOARCH) \
go build -ldflags \
"-s \
-w \
-X '${PROJECT}/cmd/version.semVersion=${RELEASE}' \
-X '${PROJECT}/cmd/version.gitCommit=${COMMIT}' \
-X '${PROJECT}/cmd/version.buildDate=${BUILD_DATE}'" \
./
RUN echo ${RELEASE}
FROM cgr.dev/chainguard/static:latest
COPY --from=builder /tmp/builder/falcoctl /usr/bin/falcoctl
ENTRYPOINT [ "/usr/bin/falcoctl" ]

78
cmd/artifact/artifact.go Normal file
View File

@ -0,0 +1,78 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 artifact
import (
"context"
"github.com/spf13/cobra"
artifactconfig "github.com/falcosecurity/falcoctl/cmd/artifact/config"
"github.com/falcosecurity/falcoctl/cmd/artifact/follow"
"github.com/falcosecurity/falcoctl/cmd/artifact/info"
"github.com/falcosecurity/falcoctl/cmd/artifact/install"
"github.com/falcosecurity/falcoctl/cmd/artifact/list"
"github.com/falcosecurity/falcoctl/cmd/artifact/manifest"
"github.com/falcosecurity/falcoctl/cmd/artifact/search"
"github.com/falcosecurity/falcoctl/internal/config"
"github.com/falcosecurity/falcoctl/pkg/index/cache"
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
)
// NewArtifactCmd return the artifact command.
func NewArtifactCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Command {
cmd := &cobra.Command{
Use: "artifact",
DisableFlagsInUseLine: true,
Short: "Interact with Falco artifacts",
Long: "Interact with Falco artifacts",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
var indexes []config.Index
var indexCache *cache.Cache
var err error
opt.Initialize()
if err = config.Load(opt.ConfigFile); err != nil {
return err
}
// add indexes if needed
// Set up basic authentication
if indexes, err = config.Indexes(); err != nil {
return err
}
// Create the index cache.
if indexCache, err = cache.NewFromConfig(ctx, config.IndexesFile, config.IndexesDir, indexes); err != nil {
return err
}
// Save the index cache for later use by the sub commands.
opt.Initialize(commonoptions.WithIndexCache(indexCache))
return nil
},
}
cmd.AddCommand(search.NewArtifactSearchCmd(ctx, opt))
cmd.AddCommand(install.NewArtifactInstallCmd(ctx, opt))
cmd.AddCommand(list.NewArtifactListCmd(ctx, opt))
cmd.AddCommand(info.NewArtifactInfoCmd(ctx, opt))
cmd.AddCommand(follow.NewArtifactFollowCmd(ctx, opt))
cmd.AddCommand(artifactconfig.NewArtifactConfigCmd(ctx, opt))
cmd.AddCommand(manifest.NewArtifactManifestCmd(ctx, opt))
return cmd
}

View File

@ -0,0 +1,93 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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
import (
"context"
"fmt"
"runtime"
"strings"
"github.com/spf13/cobra"
ocipuller "github.com/falcosecurity/falcoctl/pkg/oci/puller"
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
"github.com/falcosecurity/falcoctl/pkg/options"
)
type artifactConfigOptions struct {
*options.Common
*options.Registry
platform string
}
// NewArtifactConfigCmd returns the artifact config command.
func NewArtifactConfigCmd(ctx context.Context, opt *options.Common) *cobra.Command {
o := artifactConfigOptions{
Common: opt,
Registry: &options.Registry{},
}
cmd := &cobra.Command{
Use: "config [ref] [flags]",
Short: "Get the config layer of an artifact",
Long: "Get the config layer of an artifact",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunArtifactConfig(ctx, args)
},
}
o.Registry.AddFlags(cmd)
cmd.Flags().StringVar(&o.platform, "platform", fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
"os and architecture of the artifact in OS/ARCH format")
return cmd
}
func (o *artifactConfigOptions) RunArtifactConfig(ctx context.Context, args []string) error {
var (
puller *ocipuller.Puller
ref string
config []byte
err error
)
// Create puller with auto login enabled.
if puller, err = ociutils.Puller(o.PlainHTTP, o.Printer); err != nil {
return err
}
// Resolve the artifact reference.
if ref, err = o.IndexCache.ResolveReference(args[0]); err != nil {
return err
}
// TODO: implement two new flags (platforms, platform) based on the oci platform struct.
// Split the platform.
tokens := strings.Split(o.platform, "/")
if len(tokens) != 2 {
return fmt.Errorf("invalid platform format: %s", o.platform)
}
if config, err = puller.RawConfigLayer(ctx, ref, tokens[0], tokens[1]); err != nil {
return err
}
o.Printer.DefaultText.Println(string(config))
return nil
}

View File

@ -0,0 +1,142 @@
//SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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_test
import (
"context"
"fmt"
"net/http"
"testing"
"time"
"github.com/distribution/distribution/v3/configuration"
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/spf13/cobra"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
"github.com/falcosecurity/falcoctl/cmd"
"github.com/falcosecurity/falcoctl/pkg/oci"
"github.com/falcosecurity/falcoctl/pkg/oci/authn"
ocipusher "github.com/falcosecurity/falcoctl/pkg/oci/pusher"
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
testutils "github.com/falcosecurity/falcoctl/pkg/test"
)
var (
localRegistryHost string
localRegistry *remote.Registry
testRuleTarball = "../../../pkg/test/data/rules.tar.gz"
testPluginTarball = "../../../pkg/test/data/plugin.tar.gz"
testPluginPlatform1 = "linux/amd64"
testPluginPlatform2 = "windows/amd64"
testPluginPlatform3 = "linux/arm64"
ctx = context.Background()
pluginMultiPlatformRef string
rulesRef string
artifactWithoutConfigRef string
output = gbytes.NewBuffer()
rootCmd *cobra.Command
opt *commonoptions.Common
)
func TestConfig(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Config Suite")
}
var _ = BeforeSuite(func() {
var err error
config := &configuration.Configuration{}
// Get a free port to be used by the registry.
port, err := testutils.FreePort()
Expect(err).ToNot(HaveOccurred())
// Create the registry address to which will bind.
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
localRegistryHost = config.HTTP.Addr
// Create the oras registry.
localRegistry, err = testutils.NewOrasRegistry(localRegistryHost, true)
Expect(err).ToNot(HaveOccurred())
// Start the local registry.
go func() {
err := testutils.StartRegistry(context.Background(), config)
Expect(err).ToNot(BeNil())
}()
// Check that the registry is up and accepting connections.
Eventually(func(g Gomega) error {
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
g.Expect(err).ShouldNot(HaveOccurred())
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
return err
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
// Initialize options for command.
opt = commonoptions.NewOptions()
opt.Initialize(commonoptions.WithWriter(output))
// Push the artifacts to the registry.
// Same artifacts will be used to test the puller code.
pusher := ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), true, nil)
// Push plugin artifact with multiple architectures.
filePathsAndPlatforms := ocipusher.WithFilepathsAndPlatforms([]string{testPluginTarball, testPluginTarball, testPluginTarball},
[]string{testPluginPlatform1, testPluginPlatform2, testPluginPlatform3})
pluginMultiPlatformRef = localRegistryHost + "/plugins:multiplatform"
artConfig := oci.ArtifactConfig{}
Expect(artConfig.ParseDependencies("my-dep:1.2.3|my-alt-dep:1.4.5")).ToNot(HaveOccurred())
Expect(artConfig.ParseRequirements("my-req:7.8.9")).ToNot(HaveOccurred())
artifactConfig := ocipusher.WithArtifactConfig(artConfig)
// Build options slice.
options := []ocipusher.Option{filePathsAndPlatforms, artifactConfig}
// Push the plugin artifact.
_, err = pusher.Push(ctx, oci.Plugin, pluginMultiPlatformRef, options...)
Expect(err).ShouldNot(HaveOccurred())
// Prepare and push artifact without config layer.
filePaths := ocipusher.WithFilepaths([]string{testRuleTarball})
artConfig = oci.ArtifactConfig{}
Expect(artConfig.ParseDependencies("dep1:1.2.3", "dep2:2.3.1")).ToNot(HaveOccurred())
options = []ocipusher.Option{
filePaths,
ocipusher.WithTags("latest"),
}
// Push artifact without config layer.
// Push artifact without config layer.
artifactWithoutConfigRef = localRegistryHost + "/artifact:noconfig"
_, err = pusher.Push(ctx, oci.Rulesfile, artifactWithoutConfigRef, options...)
Expect(err).ShouldNot(HaveOccurred())
// Push a rulesfile artifact
options = append(options, ocipusher.WithArtifactConfig(artConfig))
rulesRef = localRegistryHost + "/rulesfiles:regular"
_, err = pusher.Push(ctx, oci.Rulesfile, rulesRef, options...)
Expect(err).ShouldNot(HaveOccurred())
})
func executeRoot(args []string) error {
rootCmd.SetArgs(args)
rootCmd.SetOut(output)
return cmd.Execute(rootCmd, opt)
}

View File

@ -0,0 +1,213 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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_test
import (
"regexp"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/falcosecurity/falcoctl/cmd"
)
var usage = `Usage:
falcoctl artifact config [ref] [flags]
Flags:
-h, --help help for config
--plain-http allows interacting with remote registry via plain http requests
Global Flags:
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
--log-format string Set formatting for logs (color, text, json) (default "color")
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
`
var help = `Get the config layer of an artifact
Usage:
falcoctl artifact config [ref] [flags]
Flags:
-h, --help help for config
--plain-http allows interacting with remote registry via plain http requests
--platform string os and architecture of the artifact in OS/ARCH format (default "linux/amd64")
Global Flags:
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
--log-format string Set formatting for logs (color, text, json) (default "color")
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
`
var _ = Describe("Config", func() {
const (
artifactCmd = "artifact"
configCmd = "config"
plaingHTTP = "--plain-http"
configFlag = "--config"
platformFlag = "--platform"
)
var (
err error
args []string
configDir string
)
var assertFailedBehavior = func(usage, specificError string) {
It("check that fails and the usage is not printed", func() {
Expect(err).To(HaveOccurred())
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
})
}
JustBeforeEach(func() {
configDir = GinkgoT().TempDir()
rootCmd = cmd.New(ctx, opt)
err = executeRoot(args)
})
JustAfterEach(func() {
err = nil
Expect(output.Clear()).ShouldNot(HaveOccurred())
args = nil
})
Context("help message", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, "--help"}
})
It("should match the saved one", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(string(output.Contents())).Should(Equal(help))
})
})
Context("wrong number of arguments", func() {
When("number of arguments equal to 0", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd}
})
assertFailedBehavior(usage, "ERROR accepts 1 arg(s), received 0 ")
})
When("number of arguments equal to 2", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, "arg1", "arg2", configFlag, configDir}
})
assertFailedBehavior(usage, "ERROR accepts 1 arg(s), received 2 ")
})
})
Context("failure", func() {
When("unreachable/non existing registry", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, "noregistry/noartifact", plaingHTTP, configFlag, configDir}
})
assertFailedBehavior(usage, "ERROR unable to get manifest: unable to fetch reference")
})
When("non existing repository", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, localRegistryHost + "/noartifact", plaingHTTP, configFlag, configDir}
})
assertFailedBehavior(usage, "noartifact:latest: not found")
})
When("non parsable reference", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, " ", plaingHTTP, configFlag, configDir}
})
assertFailedBehavior(usage, "ERROR cannot find among the configured indexes, skipping ")
})
When("no manifest for given platform", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir, platformFlag, "linux/wrong"}
})
assertFailedBehavior(usage, "ERROR unable to get manifest: unable to find a manifest matching the given platform: linux/wrong")
})
})
Context("success", func() {
When("empty config layer", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, artifactWithoutConfigRef, plaingHTTP, configFlag, configDir}
})
It("should success", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(output).Should(gbytes.Say(regexp.QuoteMeta("{}")))
})
})
When("with valid config layer", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, rulesRef, plaingHTTP, configFlag, configDir}
})
It("should success", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(`{"dependencies":[{"name":"dep1","version":"1.2.3"},{"name":"dep2","version":"2.3.1"}]}`)))
})
})
When("no platform flag", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir}
})
It("should success getting the platform where tests are running", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
`{"dependencies":[{"name":"my-dep","version":"1.2.3","alternatives":[{"name":"my-alt-dep","version":"`)))
})
})
When("with valid platform", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir, platformFlag, testPluginPlatform3}
})
It("should success", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
`{"dependencies":[{"name":"my-dep","version":"1.2.3","alternatives":[{"name":"my-alt-dep","version":"`)))
})
})
When("with non existing platform for artifacts without platforms", func() {
BeforeEach(func() {
args = []string{artifactCmd, configCmd, rulesRef, plaingHTTP, configFlag, configDir, platformFlag, testPluginPlatform3}
})
It("should success", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(`{"dependencies":[{"name":"dep1","version":"1.2.3"},{"name":"dep2","version":"2.3.1"}]}`)))
})
})
})
})

View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 defines the business logic to fetch config layer for artifacts.
package config

17
cmd/artifact/doc.go Normal file
View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 artifact implements the artifact commands.
package artifact

View File

@ -0,0 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 follow defines the business logic to follow artifacts. Periodically checks if there are updates
// and downlods them if any.
package follow

View File

@ -0,0 +1,500 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 follow
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"sync"
"time"
"github.com/robfig/cron/v3"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/falcosecurity/falcoctl/cmd/artifact/install"
"github.com/falcosecurity/falcoctl/internal/config"
"github.com/falcosecurity/falcoctl/internal/follower"
"github.com/falcosecurity/falcoctl/pkg/index/index"
"github.com/falcosecurity/falcoctl/pkg/oci"
"github.com/falcosecurity/falcoctl/pkg/options"
"github.com/falcosecurity/falcoctl/pkg/output"
)
const (
timeout = time.Second * 5
longFollow = `This command allows you to keep up-to-date one or more given artifacts.
It checks for updates on a periodic basis and then downloads and installs the latest version,
as specified by the passed tags.
Artifact references and flags are passed as arguments through:
- command line options
- environment variables
- configuration file
The arguments passed through these different modalities are prioritized in the following order:
command line options, environment variables, and finally the configuration file. This means that
if an argument is passed through multiple modalities, the value set in the command line options
will take precedence over the value set in environment variables, which will in turn take precedence
over the value set in the configuration file.
Please note that when passing multiple artifact references via an environment variable, they must be
separated by a semicolon ';' and the environment variable used for references is called
FALCOCT_ARTIFACT_FOLLOW_REFS. Other arguments, if passed through environment variables, should start
with "FALCOCTL_" and be followed by the hierarchical keys used in the configuration file separated by
an underscore "_".
A reference is either a simple name or a fully qualified reference ("<registry>/<repository>"),
optionally followed by ":<tag>" (":latest" is assumed by default when no tag is given).
When providing just the name of the artifact, the command will search for the artifacts in
the configured index files, and if found, it will use the registry and repository specified
in the indexes.
Example - Install and follow "latest" tag of "k8saudit-rules" artifact by relying on index metadata:
falcoctl artifact follow k8saudit-rules
Example - Install and follow all updates from "k8saudit-rules" 0.5.x release series:
falcoctl artifact follow k8saudit-rules:0.5
Example - Install and follow "cloudtrail" plugins using a fully qualified reference:
falcoctl artifact follow ghcr.io/falcosecurity/plugins/ruleset/k8saudit:latest
`
)
type artifactFollowOptions struct {
*options.Common
*options.Registry
*options.Directory
tmpDir string
every time.Duration
cron string
falcoVersions string
versions config.FalcoVersions
timeout time.Duration
closeChan chan bool
allowedTypes oci.ArtifactTypeSlice
noVerify bool
}
// NewArtifactFollowCmd returns the artifact follow command.
//
//nolint:gocyclo // unknown reason for cyclomatic complexity
func NewArtifactFollowCmd(ctx context.Context, opt *options.Common) *cobra.Command {
o := artifactFollowOptions{
Common: opt,
Registry: &options.Registry{},
Directory: &options.Directory{},
closeChan: make(chan bool),
versions: config.FalcoVersions{},
}
cmd := &cobra.Command{
Use: "follow [ref1 [ref2 ...]] [flags]",
Short: "Install a list of artifacts and continuously checks if there are updates",
Long: longFollow,
PreRunE: func(cmd *cobra.Command, args []string) error {
// Override "every" flag with viper config if not set by user.
f := cmd.Flags().Lookup("every")
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag every")
} else if !f.Changed && viper.IsSet(config.ArtifactFollowEveryKey) {
val := viper.Get(config.ArtifactFollowEveryKey)
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
return fmt.Errorf("unable to overwrite \"every\" flag: %w", err)
}
}
// Override "cron" flag with viper config if not set by user.
f = cmd.Flags().Lookup("cron")
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag cron")
} else if !f.Changed && viper.IsSet(config.ArtifactFollowCronKey) {
val := viper.Get(config.ArtifactFollowCronKey)
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
return fmt.Errorf("unable to overwrite \"cron\" flag: %w", err)
}
}
// Override "falco-versions" flag with viper config if not set by user.
f = cmd.Flags().Lookup("falco-versions")
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag falco-versions")
} else if !f.Changed && viper.IsSet(config.ArtifactFollowFalcoVersionsKey) {
val := viper.Get(config.ArtifactFollowFalcoVersionsKey)
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
return fmt.Errorf("unable to overwrite \"falco-versions\" flag: %w", err)
}
}
// Override "rulesfiles-dir" flag with viper config if not set by user.
f = cmd.Flags().Lookup(options.FlagRulesFilesDir)
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag %q", options.FlagRulesFilesDir)
} else if !f.Changed && viper.IsSet(config.ArtifactFollowRulesfilesDirKey) {
val := viper.Get(config.ArtifactFollowRulesfilesDirKey)
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagRulesFilesDir, err)
}
}
// Override "plugins-dir" flag with viper config if not set by user.
f = cmd.Flags().Lookup(options.FlagPluginsFilesDir)
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag %q", options.FlagPluginsFilesDir)
} else if !f.Changed && viper.IsSet(config.ArtifactFollowPluginsDirKey) {
val := viper.Get(config.ArtifactFollowPluginsDirKey)
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagPluginsFilesDir, err)
}
}
// Override "assets-dir" flag with viper config if not set by user.
f = cmd.Flags().Lookup(options.FlagAssetsFilesDir)
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag %q", options.FlagAssetsFilesDir)
} else if !f.Changed && viper.IsSet(config.ArtifactFollowAssetsDirKey) {
val := viper.Get(config.ArtifactFollowAssetsDirKey)
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagAssetsFilesDir, err)
}
}
// Override "tmp-dir" flag with viper config if not set by user.
f = cmd.Flags().Lookup("tmp-dir")
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag tmp-dir")
} else if !f.Changed && viper.IsSet(config.ArtifactFollowTmpDirKey) {
val := viper.Get(config.ArtifactFollowTmpDirKey)
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
return fmt.Errorf("unable to overwrite \"tmp-dir\" flag: %w", err)
}
}
// Override "allowed-types" flag with viper config if not set by user.
f = cmd.Flags().Lookup(install.FlagAllowedTypes)
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag %s", install.FlagAllowedTypes)
} else if !f.Changed && viper.IsSet(config.ArtifactAllowedTypesKey) {
val, err := config.ArtifactAllowedTypes()
if err != nil {
return err
}
if err := cmd.Flags().Set(f.Name, val.String()); err != nil {
return fmt.Errorf("unable to overwrite %q flag: %w", install.FlagAllowedTypes, err)
}
}
// Override "no-verify" flag with viper config if not set by user.
f = cmd.Flags().Lookup(install.FlagNoVerify)
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag %s", install.FlagNoVerify)
} else if !f.Changed && viper.IsSet(config.ArtifactNoVerifyKey) {
val := viper.Get(config.ArtifactNoVerifyKey)
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
return fmt.Errorf("unable to overwrite %q flag: %w", install.FlagNoVerify, err)
}
}
// Get Falco versions via HTTP endpoint
if err := o.retrieveFalcoVersions(ctx); err != nil {
return fmt.Errorf("unable to retrieve Falco versions, please check if it is running "+
"and correctly exposing the version endpoint: %w", err)
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunArtifactFollow(ctx, args)
},
}
o.Registry.AddFlags(cmd)
o.Directory.AddFlags(cmd)
cmd.Flags().DurationVarP(&o.every, "every", "e", config.FollowResync, "Time interval how often it checks for a new version of the "+
"artifact. Cannot be used together with 'cron' option.")
cmd.Flags().StringVar(&o.cron, "cron", "", "Cron-like string to specify interval how often it checks for a new version of the artifact."+
" Cannot be used together with 'every' option.")
cmd.Flags().StringVar(&o.tmpDir, "tmp-dir", "", "Directory where to save temporary files")
cmd.Flags().StringVar(&o.falcoVersions, "falco-versions", "http://localhost:8765/versions",
"Where to retrieve versions, it can be either an URL or a path to a file")
cmd.Flags().DurationVar(&o.timeout, "timeout", defaultBackoffConfig.MaxDelay,
"Timeout for initial connection to the Falco versions endpoint")
cmd.Flags().Var(&o.allowedTypes, install.FlagAllowedTypes,
fmt.Sprintf(`list of artifact types that can be followed. If not specified or configured, all types are allowed.
It accepts comma separated values or it can be repeated multiple times.
Examples:
--%s="rulesfile,plugin"
--%s=rulesfile --%s=plugin`, install.FlagAllowedTypes, install.FlagAllowedTypes, install.FlagAllowedTypes))
cmd.Flags().BoolVar(&o.noVerify, install.FlagNoVerify, false,
"whether this command should skip signature verification")
cmd.MarkFlagsMutuallyExclusive("cron", "every")
return cmd
}
// RunArtifactFollow executes the business logic for the artifact follow command.
func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []string) error {
logger := o.Printer.Logger
// Retrieve configuration for follower
configuredFollower, err := config.Follower()
if err != nil {
o.Printer.CheckErr(fmt.Errorf("unable to retrieved the configured follower: %w", err))
}
// Set args as configured if no arg was passed
if len(args) == 0 {
if len(configuredFollower.Artifacts) == 0 {
return fmt.Errorf("no artifacts to follow, please configure artifacts or pass them as arguments to this command")
}
args = configuredFollower.Artifacts
}
var sched cron.Schedule
if o.cron != "" {
sched, err = cron.ParseStandard(o.cron)
if err != nil {
return fmt.Errorf("unable to parse cron '%s': %w", o.cron, err)
}
} else {
sched = scheduledDuration{o.every}
}
var wg sync.WaitGroup
// For each artifact create a follower.
var followers = make(map[string]*follower.Follower, 0)
for _, a := range args {
if o.cron != "" {
logger.Info("Creating follower", logger.Args("artifact", a, "cron", o.cron))
} else {
logger.Info("Creating follower", logger.Args("artifact", a, "check every", o.every.String()))
}
ref, err := o.IndexCache.ResolveReference(a)
if err != nil {
return fmt.Errorf("unable to parse artifact reference for %q: %w", a, err)
}
var sig *index.Signature
if !o.noVerify {
sig = o.IndexCache.SignatureForIndexRef(a)
}
cfg := &follower.Config{
WaitGroup: &wg,
Resync: sched,
RulesfilesDir: o.RulesfilesDir,
PluginsDir: o.PluginsDir,
AssetsDir: o.AssetsDir,
ArtifactReference: ref,
PlainHTTP: o.PlainHTTP,
CloseChan: o.closeChan,
TmpDir: o.tmpDir,
FalcoVersions: o.versions,
AllowedTypes: o.allowedTypes,
Signature: sig,
}
fol, err := follower.New(ref, o.Printer, cfg)
if err != nil {
return fmt.Errorf("unable to create the follower for ref %q: %w", ref, err)
}
wg.Add(1)
followers[ref] = fol
}
for k, f := range followers {
logger.Info("Starting follower", logger.Args("artifact", k))
go f.Follow(ctx)
}
// Wait until we receive a signal to be terminated
<-ctx.Done()
// We are done, shutdown the followers.
logger.Info("Closing followers...")
close(o.closeChan)
// Wait for the followers to shutdown or that the timer expires.
doneChan := make(chan bool)
go func() {
wg.Wait()
close(doneChan)
}()
select {
case <-doneChan:
logger.Info("Followers correctly stopped.")
case <-time.After(timeout):
logger.Info("Timed out waiting for followers to exit")
}
return nil
}
func (o *artifactFollowOptions) retrieveFalcoVersions(ctx context.Context) error {
_, err := url.ParseRequestURI(o.falcoVersions)
if err != nil {
return fmt.Errorf("unable to parse URI: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, o.falcoVersions, http.NoBody)
if err != nil {
return fmt.Errorf("cannot fetch Falco version: %w", err)
}
backoffConfig := defaultBackoffConfig
backoffConfig.MaxDelay = o.timeout
client := &http.Client{
Transport: &backoffTransport{
Base: http.DefaultTransport,
Printer: o.Printer,
Config: backoffConfig,
},
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("unable to get versions from URL %q: %w", o.falcoVersions, err)
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("unable to read response body: %w", err)
}
var dataUnmarshalled map[string]interface{}
err = json.Unmarshal(data, &dataUnmarshalled)
if err != nil {
return fmt.Errorf("error unmarshalling: %w", err)
}
for key, value := range dataUnmarshalled {
// todo(alacuku): how to handle types other than strings? Silently ignoring for now...
if strValue, ok := value.(string); ok {
o.versions[key] = strValue
}
}
return nil
}
// Config defines the configuration options for backoff.
type backoffConfig struct {
// BaseDelay is the amount of time to backoff after the first failure.
BaseDelay time.Duration
// Multiplier is the factor with which to multiply backoffs after a
// failed retry. Should ideally be greater than 1.
Multiplier float64
// Jitter is the factor with which backoffs are randomized.
// todo: not yet implemented
// Jitter float64
// MaxDelay is the upper bound of backoff delay.
MaxDelay time.Duration
}
var defaultBackoffConfig = backoffConfig{
BaseDelay: 1.0 * time.Second,
Multiplier: 1.6,
// Jitter: 0.2, todo: not yet implemented
MaxDelay: 120 * time.Second,
}
type backoffTransport struct {
Base http.RoundTripper
Printer *output.Printer
Config backoffConfig
attempts int
startTime time.Time
}
func (bt *backoffTransport) RoundTrip(req *http.Request) (*http.Response, error) {
var err error
var resp *http.Response
logger := bt.Printer.Logger
bt.startTime = time.Now()
bt.attempts = 0
logger.Debug(fmt.Sprintf("Retrieving versions from Falco (timeout %s) ...", bt.Config.MaxDelay))
for {
resp, err = bt.Base.RoundTrip(req)
if err != nil {
if req.Context().Err() != nil {
return nil, req.Context().Err()
}
sleep := bt.Config.backoff(bt.attempts)
wakeTime := time.Now().Add(sleep)
if wakeTime.Sub(bt.startTime) > bt.Config.MaxDelay {
return resp, fmt.Errorf("timeout occurred while retrieving versions from Falco")
}
logger.Debug(fmt.Sprintf("error: %s. Trying again in %s", err.Error(), sleep.String()))
time.Sleep(sleep)
} else {
logger.Debug("Successfully retrieved versions from Falco")
return resp, err
}
bt.attempts++
}
}
// Backoff returns the amount of time to wait before the next retry given the
// number of retries.
func (bc backoffConfig) backoff(retries int) time.Duration {
if retries == 0 {
return bc.BaseDelay
}
backoff, max := float64(bc.BaseDelay), float64(bc.MaxDelay)
for backoff < max && retries > 0 {
backoff *= bc.Multiplier
retries--
}
if backoff > max {
backoff = max
}
// Randomize backoff delays so that if a cluster of requests start at
// the same time, they won't operate in lockstep.
// todo: implement jitter
// backoff *= 1 + bc.Jitter*(math.Float64()*2-1)
if backoff < 0 {
return 0
}
return time.Duration(backoff)
}
type scheduledDuration struct {
time.Duration
}
func (sd scheduledDuration) Next(tm time.Time) time.Time {
return tm.Add(sd.Duration)
}

17
cmd/artifact/info/doc.go Normal file
View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 info defines the business logic to get information for a given artifact.
package info

123
cmd/artifact/info/info.go Normal file
View File

@ -0,0 +1,123 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 info
import (
"context"
"errors"
"fmt"
"strings"
"github.com/spf13/cobra"
"oras.land/oras-go/v2/registry"
"github.com/falcosecurity/falcoctl/pkg/oci/repository"
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
"github.com/falcosecurity/falcoctl/pkg/options"
"github.com/falcosecurity/falcoctl/pkg/output"
)
type artifactInfoOptions struct {
*options.Common
*options.Registry
}
// NewArtifactInfoCmd returns the artifact info command.
func NewArtifactInfoCmd(ctx context.Context, opt *options.Common) *cobra.Command {
o := artifactInfoOptions{
Common: opt,
Registry: &options.Registry{},
}
cmd := &cobra.Command{
Use: "info [ref1 [ref2 ...]] [flags]",
DisableFlagsInUseLine: true,
Short: "Retrieve all available versions of a given artifact",
Long: "Retrieve all available versions of a given artifact",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunArtifactInfo(ctx, args)
},
}
o.Registry.AddFlags(cmd)
return cmd
}
func (o *artifactInfoOptions) RunArtifactInfo(ctx context.Context, args []string) error {
var data [][]string
logger := o.Printer.Logger
client, err := ociutils.Client(true)
if err != nil {
return err
}
// resolve references
for _, name := range args {
var ref string
parsedRef, err := registry.ParseReference(name)
if err != nil {
entry, ok := o.IndexCache.MergedIndexes.EntryByName(name)
if !ok {
logger.Warn("Cannot find artifact, skipping", logger.Args("name", name))
continue
}
ref = fmt.Sprintf("%s/%s", entry.Registry, entry.Repository)
} else {
parsedRef.Reference = ""
ref = parsedRef.String()
}
repo, err := repository.NewRepository(ref,
repository.WithClient(client),
repository.WithPlainHTTP(o.PlainHTTP))
if err != nil {
return err
}
tags, err := repo.Tags(ctx)
if err != nil && !errors.Is(err, context.Canceled) {
logger.Warn("Cannot retrieve tags from", logger.Args("ref", ref, "reason", err.Error()))
continue
} else if errors.Is(err, context.Canceled) {
// When the context is canceled we exit, since we receive a termination signal.
return err
}
joinedTags := strings.Join(filterOutSigTags(tags), ", ")
data = append(data, []string{ref, joinedTags})
}
// Print the table header + data only if there is data.
if len(data) > 0 {
return o.Printer.PrintTable(output.ArtifactInfo, data)
}
return nil
}
func filterOutSigTags(tags []string) []string {
// Iterate the slice in reverse to avoid index shifting when deleting
for i := len(tags) - 1; i >= 0; i-- {
if strings.HasSuffix(tags[i], ".sig") {
// Remove the element at index i by slicing the slice
tags = append(tags[:i], tags[i+1:]...)
}
}
return tags
}

View File

@ -0,0 +1,30 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 install
const (
// FlagAllowedTypes is the name of the flag to specify allowed artifact types.
FlagAllowedTypes = "allowed-types"
// FlagPlatform is the name of the flag to override the platform.
FlagPlatform = "platform"
// FlagResolveDeps is the name of the flag to enable artifact dependencies resolution.
FlagResolveDeps = "resolve-deps"
// FlagNoVerify is the name of the flag to disable signature verification.
FlagNoVerify = "no-verify"
)

View File

@ -0,0 +1,201 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 install
import (
"errors"
"fmt"
"github.com/blang/semver"
"github.com/falcosecurity/falcoctl/pkg/oci"
)
type artifactConfigResolver func(ref string) (*oci.RegistryResult, error)
type depsMapType map[string]*depInfo
var (
// ErrCannotSatisfyDependencies is the error returned when we cannot correctly resolve dependencies.
ErrCannotSatisfyDependencies = errors.New("cannot satisfy dependencies")
)
type depInfo struct {
// ref is the remote reference to this artifact
ref string
// config contains the config layer for this artifact
config *oci.ArtifactConfig
// ver represents the semver version of this artifact
ver *semver.Version
// ok is used to mark this dependency as fully processed, with its own
// dependencies and alternatives
ok bool
}
func copyDepsMap(in depsMapType) (out depsMapType) {
out = make(depsMapType, len(in))
for k, v := range in {
out[k] = v
}
return
}
// ResolveDeps resolves dependencies to a list of references.
func ResolveDeps(resolver artifactConfigResolver, inRefs ...string) (outRefs []string, err error) {
depMap := make(depsMapType)
// configMap is used to avoid getting a remote config layer more than once
configMap := make(map[string]*oci.ArtifactConfig)
retrieveConfig := func(ref string) (*oci.ArtifactConfig, error) {
config, ok := configMap[ref]
if !ok {
res, err := resolver(ref)
if err != nil {
return nil, err
}
configMap[ref] = &res.Config
return &res.Config, nil
}
return config, nil
}
upsertMap := func(ref string) error {
// fetch artifact config layer metadata
config, err := retrieveConfig(ref)
if err != nil {
return err
}
if config.Version == "" {
return fmt.Errorf("empty version for ref %q: config may be corrupted", ref)
}
ver, err := semver.Parse(config.Version)
if err != nil {
return fmt.Errorf("unable to parse version %q for ref %q, %w", config.Version, ref, err)
}
depMap[config.Name] = &depInfo{
ref: ref,
config: config,
ver: &ver,
}
return nil
}
// Prepare initial map from user inputs
for _, ref := range inRefs {
config, err := retrieveConfig(ref)
if err != nil {
return nil, err
}
name := config.Name
// todo: shall we shadow?
if info, ok := depMap[name]; ok {
return nil, fmt.Errorf(`cannot provide multiple references for %q: %q, %q`, name, info.ref, ref)
}
if err := upsertMap(ref); err != nil {
return nil, err
}
}
for {
allOk := true
// Since we are updating depMap in this for loop, let's copy the map for iterating it
// while we continue inserting new values in the real depMap map.
for name, info := range copyDepsMap(depMap) {
if info.ok {
continue
}
for _, required := range info.config.Dependencies {
// Does already exist in the map?
if existing, ok := depMap[required.Name]; ok {
requiredVer, err := semver.Parse(required.Version)
if err != nil {
return nil, fmt.Errorf(`invalid artifact config: version %q is not semver compatible`, required.Version)
}
// Is the existing dep compatible?
if existing.ver.Major != requiredVer.Major {
return nil, fmt.Errorf(
`%w: %s depends on %s:%s but an incompatible version %s:%s is required by other artifacts`,
ErrCannotSatisfyDependencies, name, required.Name, required.Version, required.Name, existing.ver.String(),
)
}
// Is required version greater than existing one?
if requiredVer.Compare(*existing.ver) <= 0 {
continue
}
}
// Are alternatives already in the map?
var foundAlternative = false
for _, alternative := range required.Alternatives {
existing, ok := depMap[alternative.Name]
if !ok {
continue
}
foundAlternative = true
alternativeVer, err := semver.Parse(alternative.Version)
if err != nil {
return nil, fmt.Errorf(`invalid artifact config: version %q is not semver compatible`, required.Version)
}
// Is the alternative specified by the user compatible?
if existing.ver.Major != alternativeVer.Major {
return nil, fmt.Errorf(
`%w: %s depends on %s:%s but an incompatible version %s:%s is required by other artifacts`,
ErrCannotSatisfyDependencies, name, required.Name, required.Version, required.Name, existing.ver.String(),
)
}
if alternativeVer.Compare(*existing.ver) > 0 {
if err := upsertMap(alternative.Name + ":" + alternativeVer.String()); err != nil {
return nil, err
}
}
break
}
if foundAlternative {
continue
}
// dep to be added or bumped
if err := upsertMap(required.Name + ":" + required.Version); err != nil {
return nil, err
}
allOk = false
}
// dep processed
info.ok = true
}
if allOk {
for _, info := range depMap {
outRefs = append(outRefs, info.ref)
}
return
}
}
}

View File

@ -0,0 +1,240 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 install
import (
"errors"
"sort"
"strings"
"testing"
"github.com/falcosecurity/falcoctl/pkg/oci"
)
type testCase struct {
scenario string
description string
inRef []string
resolver artifactConfigResolver
expectedOutRef []string
expectedErr error
}
func (t *testCase) checkOutRef(outRef []string) bool {
if len(t.expectedOutRef) != len(outRef) {
return false
}
sort.Strings(outRef)
sort.Strings(t.expectedOutRef)
for i, val := range t.expectedOutRef {
if val != outRef[i] {
return false
}
}
return true
}
func TestResolveDeps(t *testing.T) {
const (
ref1 = "ref1:0.1.2"
ref2 = "ref2:4.5.6"
dep1 = "dep1:1.2.3"
dep1Compatible = "dep1:1.3.0"
alt1 = "alt1:2.5.0"
)
testCases := []testCase{
{
scenario: "resolve one dependency",
description: "ref:0.1.2 --> dep1:1.2.3",
inRef: []string{ref1},
resolver: artifactConfigResolver(func(ref string) (*oci.RegistryResult, error) {
if ref == ref1 {
return &oci.RegistryResult{
Config: oci.ArtifactConfig{
Name: "ref1",
Version: "0.1.2",
Dependencies: []oci.ArtifactDependency{{Name: "dep1", Version: "1.2.3"}},
},
}, nil
}
splittedRef := strings.Split(ref, ":")
return &oci.RegistryResult{
Config: oci.ArtifactConfig{
Name: splittedRef[0],
Version: splittedRef[1],
// no dependencies here
},
}, nil
}),
expectedOutRef: []string{ref1, dep1},
expectedErr: nil,
},
{
scenario: "resolve common compatible dependency",
description: "ref1:0.1.2 --> dep1:1.2.3, ref2:4.5.6 --> dep1:1.3.0",
inRef: []string{ref1, ref2},
resolver: artifactConfigResolver(func(ref string) (*oci.RegistryResult, error) {
switch ref {
case ref1:
return &oci.RegistryResult{
Config: oci.ArtifactConfig{
Name: "ref1",
Version: "0.1.2",
Dependencies: []oci.ArtifactDependency{{Name: "dep1", Version: "1.2.3"}},
},
}, nil
case ref2:
return &oci.RegistryResult{
Config: oci.ArtifactConfig{
Name: "ref2",
Version: "4.5.6",
Dependencies: []oci.ArtifactDependency{{Name: "dep1", Version: "1.3.0"}},
},
}, nil
default:
splittedRef := strings.Split(ref, ":")
return &oci.RegistryResult{
Config: oci.ArtifactConfig{
Name: splittedRef[0],
Version: splittedRef[1],
// no dependencies here
},
}, nil
}
}),
expectedOutRef: []string{ref1, ref2, dep1Compatible},
expectedErr: nil,
},
{
scenario: "resolve common but not compatible dependency",
description: "ref1:0.1.2 --> dep1:1.2.3, ref2:4.5.6 --> dep1:2.3.0",
inRef: []string{ref1, ref2},
resolver: artifactConfigResolver(func(ref string) (*oci.RegistryResult, error) {
switch ref {
case ref1:
return &oci.RegistryResult{
Config: oci.ArtifactConfig{
Name: "ref1",
Version: "0.1.2",
Dependencies: []oci.ArtifactDependency{{Name: "dep1", Version: "1.2.3"}},
},
}, nil
case ref2:
return &oci.RegistryResult{
Config: oci.ArtifactConfig{
Name: "ref2",
Version: "4.5.6",
Dependencies: []oci.ArtifactDependency{{Name: "dep1", Version: "2.3.0"}},
},
}, nil
default:
splittedRef := strings.Split(ref, ":")
return &oci.RegistryResult{
Config: oci.ArtifactConfig{
Name: splittedRef[0],
Version: splittedRef[1],
// no dependencies here
},
}, nil
}
}),
expectedOutRef: nil,
expectedErr: ErrCannotSatisfyDependencies,
},
{
scenario: "resolve compatible alternative",
description: "ref1:0.1.2 --> dep1:1.2.3 | alt1:2.5.0",
inRef: []string{ref1, alt1},
resolver: artifactConfigResolver(func(ref string) (*oci.RegistryResult, error) {
if ref == ref1 {
return &oci.RegistryResult{
Config: oci.ArtifactConfig{
Name: "ref1",
Version: "0.1.2",
Dependencies: []oci.ArtifactDependency{
{
Name: "dep1",
Version: "1.2.3",
Alternatives: []oci.Dependency{{Name: "alt1", Version: "2.3.0"}},
}},
},
}, nil
}
splittedRef := strings.Split(ref, ":")
return &oci.RegistryResult{
Config: oci.ArtifactConfig{
Name: splittedRef[0],
Version: splittedRef[1],
// no dependencies here
},
}, nil
}),
expectedOutRef: []string{ref1, alt1},
expectedErr: ErrCannotSatisfyDependencies,
},
{
scenario: "resolve not compatible alternative",
description: "ref1:0.1.2 --> dep1:1.2.3 | alt1:3.0.0",
inRef: []string{ref1, "alt1:3.0.0"},
resolver: artifactConfigResolver(func(ref string) (*oci.RegistryResult, error) {
if ref == ref1 {
return &oci.RegistryResult{
Config: oci.ArtifactConfig{
Name: "ref1",
Version: "0.1.2",
Dependencies: []oci.ArtifactDependency{
{
Name: "dep1",
Version: "1.2.3",
Alternatives: []oci.Dependency{{Name: "alt1", Version: "2.3.0"}},
}},
},
}, nil
}
splittedRef := strings.Split(ref, ":")
return &oci.RegistryResult{
Config: oci.ArtifactConfig{
Name: splittedRef[0],
Version: splittedRef[1],
// no dependencies here
},
}, nil
}),
expectedOutRef: nil,
expectedErr: ErrCannotSatisfyDependencies,
},
}
for _, testCase := range testCases {
outRef, err := ResolveDeps(testCase.resolver, testCase.inRef...)
if err != nil && !errors.Is(err, testCase.expectedErr) {
t.Fatalf("unexpected error in scenario %q, %q: %v",
testCase.scenario, testCase.description, err)
}
if !testCase.checkOutRef(outRef) {
t.Fatalf("dependencies not correctly resolved in scenario %q, %q:\n got %v, expected %v",
testCase.scenario, testCase.description, outRef, testCase.expectedOutRef)
}
}
}

View File

@ -0,0 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 install defines options, and logic used to pull an artifact from a remote repository
// and install it in the local system.
package install

View File

@ -0,0 +1,374 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 install
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/falcosecurity/falcoctl/internal/config"
"github.com/falcosecurity/falcoctl/internal/signature"
"github.com/falcosecurity/falcoctl/internal/utils"
"github.com/falcosecurity/falcoctl/pkg/index/index"
"github.com/falcosecurity/falcoctl/pkg/oci"
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
"github.com/falcosecurity/falcoctl/pkg/options"
)
const (
longInstall = `This command allows you to install one or more given artifacts.
Artifact references and flags are passed as arguments through:
- command line options
- environment variables
- configuration file
The arguments passed through these different modalities are prioritized in the following order:
command line options, environment variables, and finally the configuration file. This means that
if an argument is passed through multiple modalities, the value set in the command line options
will take precedence over the value set in environment variables, which will in turn take precedence
over the value set in the configuration file.
Please note that when passing multiple artifact references via an environment variable, they must be
separated by a semicolon ';'. Other arguments, if passed through environment variables, should start
with "FALCOCTL_" and be followed by the hierarchical keys used in the configuration file separated by
an underscore "_".
A reference is either a simple name or a fully qualified reference ("<registry>/<repository>"),
optionally followed by ":<tag>" (":latest" is assumed by default when no tag is given).
When providing just the name of the artifact, the command will search for the artifacts in
the configured index files, and if found, it will use the registry and repository specified
in the indexes.
Example - Install "latest" tag of "k8saudit-rules" artifact by relying on index metadata:
falcoctl artifact install k8saudit-rules
Example - Install all updates from "k8saudit-rules" 0.5.x release series:
falcoctl artifact install k8saudit-rules:0.5
Example - Install "cloudtrail" plugins using a fully qualified reference:
falcoctl artifact install ghcr.io/falcosecurity/plugins/ruleset/k8saudit:latest
`
)
type artifactInstallOptions struct {
*options.Common
*options.Registry
*options.Directory
allowedTypes oci.ArtifactTypeSlice
platform string // Raw string from command line
platformArch string // Architecture portion of parsed platform string
platformOS string // OS portion of parsed platform string
resolveDeps bool
noVerify bool
}
// NewArtifactInstallCmd returns the artifact install command.
func NewArtifactInstallCmd(ctx context.Context, opt *options.Common) *cobra.Command {
o := artifactInstallOptions{
Common: opt,
Registry: &options.Registry{},
Directory: &options.Directory{},
}
cmd := &cobra.Command{
Use: "install [ref1 [ref2 ...]] [flags]",
DisableFlagsInUseLine: true,
Short: "Install a list of artifacts",
Long: longInstall,
PreRunE: func(cmd *cobra.Command, args []string) error {
// Override "rulesfiles-dir" flag with viper config if not set by user.
f := cmd.Flags().Lookup(options.FlagRulesFilesDir)
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag %q", options.FlagRulesFilesDir)
} else if !f.Changed && viper.IsSet(config.ArtifactInstallRulesfilesDirKey) {
val := viper.Get(config.ArtifactInstallRulesfilesDirKey)
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagRulesFilesDir, err)
}
}
// Override "plugins-dir" flag with viper config if not set by user.
f = cmd.Flags().Lookup(options.FlagPluginsFilesDir)
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag %q", options.FlagPluginsFilesDir)
} else if !f.Changed && viper.IsSet(config.ArtifactInstallPluginsDirKey) {
val := viper.Get(config.ArtifactInstallPluginsDirKey)
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagPluginsFilesDir, err)
}
}
// Override "assets-dir" flag with viper config if not set by user.
f = cmd.Flags().Lookup(options.FlagAssetsFilesDir)
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag %q", options.FlagAssetsFilesDir)
} else if !f.Changed && viper.IsSet(config.ArtifactFollowAssetsDirKey) {
val := viper.Get(config.ArtifactFollowAssetsDirKey)
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagAssetsFilesDir, err)
}
}
// Override "allowed-types" flag with viper config if not set by user.
f = cmd.Flags().Lookup(FlagAllowedTypes)
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag %q", FlagAllowedTypes)
} else if !f.Changed && viper.IsSet(config.ArtifactAllowedTypesKey) {
val, err := config.ArtifactAllowedTypes()
if err != nil {
return err
}
if err := cmd.Flags().Set(f.Name, val.String()); err != nil {
return fmt.Errorf("unable to overwrite %s flag: %w", FlagAllowedTypes, err)
}
}
f = cmd.Flags().Lookup(FlagResolveDeps)
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag %q", FlagResolveDeps)
} else if !f.Changed && viper.IsSet(config.ArtifactInstallResolveDepsKey) {
val := viper.Get(config.ArtifactInstallResolveDepsKey)
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
return fmt.Errorf("unable to overwrite %q flag: %w", FlagResolveDeps, err)
}
}
f = cmd.Flags().Lookup(FlagNoVerify)
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag %q", FlagNoVerify)
} else if !f.Changed && viper.IsSet(config.ArtifactNoVerifyKey) {
val := viper.Get(config.ArtifactNoVerifyKey)
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
return fmt.Errorf("unable to overwrite %q flag: %w", FlagNoVerify, err)
}
}
// Parse "platform" into OS and Arch
if len(o.platform) > 0 {
parts := strings.Split(o.platform, "/")
if len(parts) != 2 {
return fmt.Errorf("invalid %q: must be in the format OS/Arch", FlagPlatform)
}
o.platformOS, o.platformArch = parts[0], parts[1]
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunArtifactInstall(ctx, args)
},
}
o.Registry.AddFlags(cmd)
o.Directory.AddFlags(cmd)
cmd.Flags().Var(&o.allowedTypes, FlagAllowedTypes,
fmt.Sprintf(`list of artifact types that can be installed. If not specified or configured, all types are allowed.
It accepts comma separated values or it can be repeated multiple times.
Examples:
--%s="rulesfile,plugin"
--%s=rulesfile --%s=plugin`, FlagAllowedTypes, FlagAllowedTypes, FlagAllowedTypes))
cmd.Flags().StringVar(&o.platform, "platform", fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
"os and architecture of the artifact in OS/ARCH format")
cmd.Flags().BoolVar(&o.resolveDeps, FlagResolveDeps, true,
"whether this command should resolve dependencies or not")
cmd.Flags().BoolVar(&o.noVerify, FlagNoVerify, false,
"whether this command should skip signature verification")
return cmd
}
// RunArtifactInstall executes the business logic for the artifact install command.
func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []string) error {
logger := o.Printer.Logger
// Retrieve configuration for installer
configuredInstaller, err := config.Installer()
if err != nil {
return fmt.Errorf("unable to retrieve the configured installer: %w", err)
}
// Set args as configured if no arg was passed
if len(args) == 0 {
if len(configuredInstaller.Artifacts) == 0 {
return fmt.Errorf("no artifacts to install, please configure artifacts or pass them as arguments to this command")
}
args = configuredInstaller.Artifacts
}
// Create temp dir where to put pulled artifacts
tmpDir, err := os.MkdirTemp("", "falcoctl")
if err != nil {
return fmt.Errorf("cannot create temporary directory: %w", err)
}
defer os.RemoveAll(tmpDir)
// Create registry puller with auto login enabled
puller, err := ociutils.Puller(o.PlainHTTP, o.Printer)
if err != nil {
return err
}
// Specify how to pull config layer for each artifact requested by user.
resolver := artifactConfigResolver(func(ref string) (*oci.RegistryResult, error) {
ref, err := o.IndexCache.ResolveReference(ref)
if err != nil {
return nil, err
}
artifactConfig, err := puller.ArtifactConfig(ctx, ref, o.platformOS, o.platformArch)
if err != nil {
return nil, err
}
return &oci.RegistryResult{
Config: *artifactConfig,
}, nil
})
signatures := make(map[string]*index.Signature)
// Compute input to install dependencies
for i, arg := range args {
ref, err := o.IndexCache.ResolveReference(arg)
if err != nil {
return err
}
if sig := o.IndexCache.SignatureForIndexRef(arg); sig != nil {
signatures[ref] = sig
}
args[i] = ref
}
var refs []string
if o.resolveDeps {
// Solve dependencies
logger.Info("Resolving dependencies ...")
refs, err = ResolveDeps(resolver, args...)
if err != nil {
return err
}
} else {
refs = args
}
logger.Info("Installing artifacts", logger.Args("refs", refs))
for _, ref := range refs {
resolvedRef, err := o.IndexCache.ResolveReference(ref)
if err != nil {
return err
}
if signatures[resolvedRef] == nil {
if sig := o.IndexCache.SignatureForIndexRef(ref); sig != nil {
signatures[resolvedRef] = sig
}
}
logger.Info("Preparing to pull artifact", logger.Args("ref", resolvedRef))
if err := puller.CheckAllowedType(ctx, resolvedRef, o.platformOS, o.platformArch, o.allowedTypes.Types); err != nil {
return err
}
// Install will always install artifact for the current OS and architecture
result, err := puller.Pull(ctx, resolvedRef, tmpDir, o.platformOS, o.platformArch)
if err != nil {
return err
}
sig := signatures[resolvedRef]
if sig != nil && !o.noVerify {
repo, err := utils.RepositoryFromRef(resolvedRef)
if err != nil {
return err
}
// In order to prevent TOCTOU issues we'll perform signature verification after we complete a pull
// and obtained a digest but before files are written to disk. This way we ensure that we're verifying
// the exact digest that we just pulled, even if the tag gets overwritten in the meantime.
digestRef := fmt.Sprintf("%s@%s", repo, result.RootDigest)
logger.Info("Verifying signature for artifact", logger.Args("digest", digestRef))
err = signature.Verify(ctx, digestRef, sig)
if err != nil {
return fmt.Errorf("error while verifying signature for %s: %w", digestRef, err)
}
logger.Info("Signature successfully verified!")
}
var destDir string
switch result.Type {
case oci.Plugin:
destDir = o.PluginsDir
case oci.Rulesfile:
destDir = o.RulesfilesDir
case oci.Asset:
destDir = o.AssetsDir
default:
return fmt.Errorf("unrecognized result type %q while pulling artifact", result.Type)
}
// Check if directory exists and is writable.
err = utils.ExistsAndIsWritable(destDir)
if err != nil {
return fmt.Errorf("cannot use directory %q as install destination: %w", destDir, err)
}
logger.Info("Extracting and installing artifact", logger.Args("type", result.Type, "file", result.Filename))
if !o.Printer.DisableStyling {
o.Printer.Spinner, _ = o.Printer.Spinner.Start("Extracting and installing")
}
result.Filename = filepath.Join(tmpDir, result.Filename)
f, err := os.Open(result.Filename)
if err != nil {
return err
}
// Extract artifact and move it to its destination directory
_, err = utils.ExtractTarGz(ctx, f, destDir, 0)
if err != nil {
return fmt.Errorf("cannot extract %q to %q: %w", result.Filename, destDir, err)
}
err = os.Remove(result.Filename)
if err != nil {
return err
}
if o.Printer.Spinner != nil {
_ = o.Printer.Spinner.Stop()
}
logger.Info("Artifact successfully installed", logger.Args("name", resolvedRef, "type", result.Type, "digest", result.Digest, "directory", destDir))
}
return nil
}

View File

@ -0,0 +1,111 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 install_test
import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"testing"
"time"
"github.com/distribution/distribution/v3/configuration"
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/spf13/cobra"
"oras.land/oras-go/v2/registry/remote"
"github.com/falcosecurity/falcoctl/cmd"
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
testutils "github.com/falcosecurity/falcoctl/pkg/test"
)
//nolint:unused // false positive
const (
rulesfiletgz = "../../../pkg/test/data/rules.tar.gz"
rulesfileyaml = "../../../pkg/test/data/rules.yaml"
plugintgz = "../../../pkg/test/data/plugin.tar.gz"
)
//nolint:unused // false positive
var (
registry string
ctx = context.Background()
output = gbytes.NewBuffer()
rootCmd *cobra.Command
opt *commonoptions.Common
port int
orasRegistry *remote.Registry
configFile string
err error
args []string
)
func TestInstall(t *testing.T) {
var err error
RegisterFailHandler(Fail)
port, err = testutils.FreePort()
Expect(err).ToNot(HaveOccurred())
registry = fmt.Sprintf("localhost:%d", port)
RunSpecs(t, "root suite")
}
var _ = BeforeSuite(func() {
config := &configuration.Configuration{}
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
// Create and configure the common options.
opt = commonoptions.NewOptions()
opt.Initialize(commonoptions.WithWriter(output))
// Create the oras registry.
orasRegistry, err = testutils.NewOrasRegistry(registry, true)
Expect(err).ToNot(HaveOccurred())
// Start the local registry.
go func() {
err := testutils.StartRegistry(context.Background(), config)
Expect(err).ToNot(BeNil())
}()
// Check that the registry is up and accepting connections.
Eventually(func(g Gomega) error {
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
g.Expect(err).ShouldNot(HaveOccurred())
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
return err
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
// Create temporary directory used to save the configuration file.
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
Expect(err).Should(Succeed())
})
var _ = AfterSuite(func() {
configDir := filepath.Dir(configFile)
Expect(os.RemoveAll(configDir)).Should(Succeed())
})
//nolint:unused // false positive
func executeRoot(args []string) error {
rootCmd.SetArgs(args)
rootCmd.SetOut(output)
return cmd.Execute(rootCmd, opt)
}

View File

@ -0,0 +1,465 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 install_test
import (
"fmt"
"os"
"path/filepath"
"regexp"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"oras.land/oras-go/v2/registry/remote/auth"
"github.com/falcosecurity/falcoctl/cmd"
"github.com/falcosecurity/falcoctl/pkg/oci"
"github.com/falcosecurity/falcoctl/pkg/oci/authn"
ocipusher "github.com/falcosecurity/falcoctl/pkg/oci/pusher"
out "github.com/falcosecurity/falcoctl/pkg/output"
)
//nolint:lll,unused // no need to check for line length.
var artifactInstallUsage = `Usage:
falcoctl artifact install [ref1 [ref2 ...]] [flags]
Flags:
--allowed-types ArtifactTypeSlice list of artifact types that can be installed. If not specified or configured, all types are allowed.
It accepts comma separated values or it can be repeated multiple times.
Examples:
--allowed-types="rulesfile,plugin"
--allowed-types=rulesfile --allowed-types=plugin
-h, --help help for install
--plain-http allows interacting with remote registry via plain http requests
--platform string os and architecture of the artifact in OS/ARCH format (default "linux/amd64")
--plugins-dir string directory where to install plugins. (default "/usr/share/falco/plugins")
--resolve-deps whether this command should resolve dependencies or not (default true)
--rulesfiles-dir string directory where to install rules. (default "/etc/falco")
Global Flags:
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
--log-format string Set formatting for logs (color, text, json) (default "color")
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
`
//nolint:unused // false positive
var artifactInstallHelp = `This command allows you to install one or more given artifacts.
Artifact references and flags are passed as arguments through:
- command line options
- environment variables
- configuration file
The arguments passed through these different modalities are prioritized in the following order:
command line options, environment variables, and finally the configuration file. This means that
if an argument is passed through multiple modalities, the value set in the command line options
will take precedence over the value set in environment variables, which will in turn take precedence
over the value set in the configuration file.
Please note that when passing multiple artifact references via an environment variable, they must be
separated by a semicolon ';'. Other arguments, if passed through environment variables, should start
with "FALCOCTL_" and be followed by the hierarchical keys used in the configuration file separated by
an underscore "_".
A reference is either a simple name or a fully qualified reference ("<registry>/<repository>"),
optionally followed by ":<tag>" (":latest" is assumed by default when no tag is given).
When providing just the name of the artifact, the command will search for the artifacts in
the configured index files, and if found, it will use the registry and repository specified
in the indexes.
Example - Install "latest" tag of "k8saudit-rules" artifact by relying on index metadata:
falcoctl artifact install k8saudit-rules
Example - Install all updates from "k8saudit-rules" 0.5.x release series:
falcoctl artifact install k8saudit-rules:0.5
Example - Install "cloudtrail" plugins using a fully qualified reference:
falcoctl artifact install ghcr.io/falcosecurity/plugins/ruleset/k8saudit:latest
`
//nolint:unused // false positive
var correctIndexConfig = `indexes:
- name: falcosecurity
url: https://falcosecurity.github.io/falcoctl/index.yaml
`
//nolint:unused // false positive
var installAssertFailedBehavior = func(usage, specificError string) {
It("check that fails and the usage is not printed", func() {
Expect(err).To(HaveOccurred())
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
})
}
//nolint:unused // false positive
var artifactInstallTests = Describe("install", func() {
var (
pusher *ocipusher.Pusher
ref string
config ocipusher.Option
)
const (
// Used as flags for all the test cases.
artifactCmd = "artifact"
installCmd = "install"
dep1 = "myplugin:1.2.3"
dep2 = "myplugin1:1.2.3|otherplugin:3.2.1"
req = "engine_version:15"
anSource = "myrepo.com/rules.git"
artifact = "generic-repo"
repo = "/" + artifact
tag = "tag"
repoAndTag = repo + ":" + tag
)
// Each test gets its own root command and runs it.
// The err variable is asserted by each test.
JustBeforeEach(func() {
rootCmd = cmd.New(ctx, opt)
err = executeRoot(args)
})
JustAfterEach(func() {
Expect(output.Clear()).ShouldNot(HaveOccurred())
})
Context("help message", func() {
BeforeEach(func() {
args = []string{artifactCmd, installCmd, "--help"}
})
It("should match the saved one", func() {
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(artifactInstallHelp)))
})
})
Context("failure", func() {
var (
tracker out.Tracker
options []ocipusher.Option
filePathsAndPlatforms ocipusher.Option
filePaths ocipusher.Option
destDir string
)
const (
plainHTTP = true
testPluginPlatform1 = "linux/amd64"
)
When("without artifact", func() {
BeforeEach(func() {
configDir := GinkgoT().TempDir()
configFile := filepath.Join(configDir, ".config")
_, err := os.Create(configFile)
Expect(err).To(BeNil())
args = []string{artifactCmd, installCmd, "--config", configFile}
})
installAssertFailedBehavior(artifactInstallUsage,
"ERROR no artifacts to install, please configure artifacts or pass them as arguments to this command")
})
When("unreachable registry", func() {
BeforeEach(func() {
configDir := GinkgoT().TempDir()
configFile := filepath.Join(configDir, ".config")
_, err := os.Create(configFile)
Expect(err).To(BeNil())
args = []string{artifactCmd, installCmd, "noregistry/testrules", "--plain-http", "--config", configFile}
})
installAssertFailedBehavior(artifactInstallUsage, `ERROR unable to get manifest: unable to fetch reference`)
})
When("invalid repository", func() {
newReg := registry + "/wrong:latest"
BeforeEach(func() {
configDir := GinkgoT().TempDir()
configFile := filepath.Join(configDir, ".config")
_, err := os.Create(configFile)
Expect(err).To(BeNil())
args = []string{artifactCmd, installCmd, newReg, "--plain-http", "--config", configFile}
})
installAssertFailedBehavior(artifactInstallUsage, fmt.Sprintf("ERROR unable to get manifest: unable to fetch reference %q", newReg))
})
When("with disallowed types (rulesfile)", func() {
BeforeEach(func() {
baseDir := GinkgoT().TempDir()
configFilePath := baseDir + "/config.yaml"
content := []byte(correctIndexConfig)
err := os.WriteFile(configFilePath, content, 0o644)
Expect(err).To(BeNil())
// push plugin
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
ref = registry + repoAndTag
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
Name: "plugin1",
Version: "0.0.1",
})
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
options = []ocipusher.Option{filePathsAndPlatforms, config}
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
Expect(err).To(BeNil())
Expect(result).ToNot(BeNil())
ref = registry + repoAndTag
Expect(err).To(BeNil())
args = []string{artifactCmd, installCmd, ref, "--plain-http", "--platform", testPluginPlatform1,
"--config", configFilePath, "--allowed-types", "rulesfile"}
})
installAssertFailedBehavior(artifactInstallUsage, "ERROR cannot download artifact of type \"plugin\": type not permitted")
})
When("with disallowed types (plugin)", func() {
BeforeEach(func() {
baseDir := GinkgoT().TempDir()
configFilePath := baseDir + "/config.yaml"
content := []byte(correctIndexConfig)
err := os.WriteFile(configFilePath, content, 0o644)
Expect(err).To(BeNil())
// push rulesfile
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
ref = registry + repoAndTag
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
Name: "rules1",
Version: "0.0.1",
})
filePaths = ocipusher.WithFilepaths([]string{rulesfiletgz})
options = []ocipusher.Option{filePaths, config}
result, err := pusher.Push(ctx, oci.Rulesfile, ref, options...)
Expect(err).To(BeNil())
Expect(result).ToNot(BeNil())
ref = registry + repoAndTag
Expect(err).To(BeNil())
args = []string{artifactCmd, installCmd, ref, "--plain-http",
"--config", configFilePath, "--allowed-types", "plugin"}
})
installAssertFailedBehavior(artifactInstallUsage, "ERROR cannot download artifact of type \"rulesfile\": type not permitted")
})
When("an unknown type is used", func() {
wrongType := "mywrongtype"
BeforeEach(func() {
baseDir := GinkgoT().TempDir()
configFilePath := baseDir + "/config.yaml"
content := []byte(correctIndexConfig)
err := os.WriteFile(configFilePath, content, 0o644)
Expect(err).To(BeNil())
// push rulesfile
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
ref = registry + repoAndTag
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
Name: "rules1",
Version: "0.0.1",
})
filePaths = ocipusher.WithFilepaths([]string{rulesfiletgz})
options = []ocipusher.Option{filePaths, config}
result, err := pusher.Push(ctx, oci.Rulesfile, ref, options...)
Expect(err).To(BeNil())
Expect(result).ToNot(BeNil())
ref = registry + repoAndTag
Expect(err).To(BeNil())
args = []string{artifactCmd, installCmd, ref, "--plain-http",
"--config", configFilePath, "--allowed-types", "plugin," + wrongType}
})
installAssertFailedBehavior(artifactInstallUsage, fmt.Sprintf("ERROR invalid argument \"plugin,%s\" for \"--allowed-types\" flag: "+
"not valid token %q: must be one of \"rulesfile\", \"plugin\"", wrongType, wrongType))
})
When("--plugins-dir is not writable", func() {
BeforeEach(func() {
destDir = GinkgoT().TempDir()
err = os.Chmod(destDir, 0o555)
Expect(err).To(BeNil())
baseDir := GinkgoT().TempDir()
configFilePath := baseDir + "/config.yaml"
content := []byte(correctIndexConfig)
err := os.WriteFile(configFilePath, content, 0o644)
Expect(err).To(BeNil())
// push plugin
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
ref = registry + repoAndTag
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
Name: "plugin1",
Version: "0.0.1",
})
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
options = []ocipusher.Option{filePathsAndPlatforms, config}
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
Expect(err).To(BeNil())
Expect(result).ToNot(BeNil())
ref = registry + repoAndTag
Expect(err).To(BeNil())
args = []string{artifactCmd, installCmd, ref, "--plain-http", "--platform", testPluginPlatform1,
"--config", configFilePath, "--plugins-dir", destDir}
})
It("check that fails and the usage is not printed", func() {
expectedError := fmt.Sprintf("ERROR cannot use directory %q "+
"as install destination: %s is not writable", destDir, destDir)
Expect(err).To(HaveOccurred())
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
})
})
When("--plugins-dir is not present", func() {
BeforeEach(func() {
destDir = GinkgoT().TempDir()
err = os.Remove(destDir)
Expect(err).To(BeNil())
baseDir := GinkgoT().TempDir()
configFilePath := baseDir + "/config.yaml"
content := []byte(correctIndexConfig)
err := os.WriteFile(configFilePath, content, 0o644)
Expect(err).To(BeNil())
// push plugin
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
ref = registry + repoAndTag
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
Name: "plugin1",
Version: "0.0.1",
})
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
options = []ocipusher.Option{filePathsAndPlatforms, config}
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
Expect(err).To(BeNil())
Expect(result).ToNot(BeNil())
ref = registry + repoAndTag
Expect(err).To(BeNil())
args = []string{artifactCmd, installCmd, ref, "--plain-http", "--platform", testPluginPlatform1,
"--config", configFilePath, "--plugins-dir", destDir}
})
It("check that fails and the usage is not printed", func() {
expectedError := fmt.Sprintf("ERROR cannot use directory %q "+
"as install destination: %s doesn't exists", destDir, destDir)
Expect(err).To(HaveOccurred())
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
})
})
When("--rulesfile-dir is not writable", func() {
BeforeEach(func() {
destDir = GinkgoT().TempDir()
err = os.Chmod(destDir, 0o555)
Expect(err).To(BeNil())
baseDir := GinkgoT().TempDir()
configFilePath := baseDir + "/config.yaml"
content := []byte(correctIndexConfig)
err := os.WriteFile(configFilePath, content, 0o644)
Expect(err).To(BeNil())
// push plugin
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
ref = registry + repoAndTag
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
Name: "rules1",
Version: "0.0.1",
})
filePaths = ocipusher.WithFilepaths([]string{rulesfiletgz})
options = []ocipusher.Option{filePaths, config}
result, err := pusher.Push(ctx, oci.Rulesfile, ref, options...)
Expect(err).To(BeNil())
Expect(result).ToNot(BeNil())
ref = registry + repoAndTag
Expect(err).To(BeNil())
args = []string{artifactCmd, installCmd, ref, "--plain-http",
"--config", configFilePath, "--rulesfiles-dir", destDir}
})
It("check that fails and the usage is not printed", func() {
expectedError := fmt.Sprintf("ERROR cannot use directory %q "+
"as install destination: %s is not writable", destDir, destDir)
Expect(err).To(HaveOccurred())
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
})
})
When("not existing --plugins-dir", func() {
BeforeEach(func() {
destDir = GinkgoT().TempDir()
err = os.Remove(destDir)
Expect(err).To(BeNil())
baseDir := GinkgoT().TempDir()
configFilePath := baseDir + "/config.yaml"
content := []byte(correctIndexConfig)
err := os.WriteFile(configFilePath, content, 0o644)
Expect(err).To(BeNil())
// push plugin
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
ref = registry + repoAndTag
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{
Name: "rules1",
Version: "0.0.1",
})
filePathsAndPlatforms = ocipusher.WithFilepaths([]string{rulesfiletgz})
options = []ocipusher.Option{filePaths, config}
result, err := pusher.Push(ctx, oci.Rulesfile, ref, options...)
Expect(err).To(BeNil())
Expect(result).ToNot(BeNil())
ref = registry + repoAndTag
Expect(err).To(BeNil())
args = []string{artifactCmd, installCmd, ref, "--plain-http",
"--config", configFilePath, "--rulesfiles-dir", destDir}
})
It("check that fails and the usage is not printed", func() {
expectedError := fmt.Sprintf("ERROR cannot use directory %q "+
"as install destination: %s doesn't exists", destDir, destDir)
Expect(err).To(HaveOccurred())
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
})
})
When("not --platform is not of the correct format", func() {
BeforeEach(func() {
destDir = GinkgoT().TempDir()
err = os.Remove(destDir)
Expect(err).To(BeNil())
baseDir := GinkgoT().TempDir()
configFilePath := baseDir + "/config.yaml"
content := []byte(correctIndexConfig)
err := os.WriteFile(configFilePath, content, 0o644)
Expect(err).To(BeNil())
ref = registry + repoAndTag
args = []string{artifactCmd, installCmd, ref, "--config", configFile, "--platform", "this/is/invalid"}
})
It("check that fails and the usage is not printed", func() {
expectedError := `ERROR invalid "platform": must be in the format OS/Arch`
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
})
})
})
})

View File

@ -0,0 +1,78 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 list
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/falcosecurity/falcoctl/pkg/oci"
"github.com/falcosecurity/falcoctl/pkg/options"
"github.com/falcosecurity/falcoctl/pkg/output"
)
// CommandName name of the command. It has to be the first word in the use line.
const CommandName = "list"
type artifactListOptions struct {
*options.Common
artifactType oci.ArtifactType
index string
}
// NewArtifactListCmd returns the artifact search command.
func NewArtifactListCmd(ctx context.Context, opt *options.Common) *cobra.Command {
o := artifactListOptions{
Common: opt,
}
cmd := &cobra.Command{
Use: fmt.Sprintf("%s [flags]", CommandName),
DisableFlagsInUseLine: true,
Short: "List all artifacts",
Long: "List all artifacts",
Aliases: []string{"ls"},
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunArtifactList(ctx, args)
},
}
cmd.Flags().Var(&o.artifactType, "type", `Only list artifacts with a specific type. Allowed values: "rulesfile", "plugin", "asset"`)
cmd.Flags().StringVar(&o.index, "index", "", "Only display artifacts from a configured index")
return cmd
}
func (o *artifactListOptions) RunArtifactList(_ context.Context, _ []string) error {
var data [][]string
for _, entry := range o.IndexCache.MergedIndexes.Entries {
if o.artifactType != "" && o.artifactType != oci.ArtifactType(entry.Type) {
continue
}
indexName := o.IndexCache.MergedIndexes.IndexByEntry(entry).Name
if o.index != "" && o.index != indexName {
continue
}
row := []string{indexName, entry.Name, entry.Type, entry.Registry, entry.Repository}
data = append(data, row)
}
return o.Printer.PrintTable(output.ArtifactSearch, data)
}

17
cmd/artifact/list/doc.go Normal file
View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 list defines the logic to list artifacts in the configured index files.
package list

View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 manifest defines the business logic to fetch manifest layer for artifacts.
package manifest

View File

@ -0,0 +1,93 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 manifest
import (
"context"
"fmt"
"runtime"
"strings"
"github.com/spf13/cobra"
ocipuller "github.com/falcosecurity/falcoctl/pkg/oci/puller"
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
"github.com/falcosecurity/falcoctl/pkg/options"
)
type artifactManifestOptions struct {
*options.Common
*options.Registry
platform string
}
// NewArtifactManifestCmd returns the artifact manifest command.
func NewArtifactManifestCmd(ctx context.Context, opt *options.Common) *cobra.Command {
o := artifactManifestOptions{
Common: opt,
Registry: &options.Registry{},
}
cmd := &cobra.Command{
Use: "manifest [ref] [flags]",
Short: "Get the manifest layer of an artifact",
Long: "Get the manifest layer of an artifact",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunArtifactManifest(ctx, args)
},
}
o.Registry.AddFlags(cmd)
cmd.Flags().StringVar(&o.platform, "platform", fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
"os and architecture of the artifact in OS/ARCH format")
return cmd
}
func (o *artifactManifestOptions) RunArtifactManifest(ctx context.Context, args []string) error {
var (
puller *ocipuller.Puller
ref string
manifest []byte
err error
)
// Create puller with auto login enabled.
if puller, err = ociutils.Puller(o.PlainHTTP, o.Printer); err != nil {
return err
}
// Resolve the artifact reference.
if ref, err = o.IndexCache.ResolveReference(args[0]); err != nil {
return err
}
// TODO: implement two new flags (platforms, platform) based on the oci platform struct.
// Split the platform.
tokens := strings.Split(o.platform, "/")
if len(tokens) != 2 {
return fmt.Errorf("invalid platform format: %s", o.platform)
}
if manifest, err = puller.RawManifest(ctx, ref, tokens[0], tokens[1]); err != nil {
return err
}
o.Printer.DefaultText.Println(string(manifest))
return nil
}

View File

@ -0,0 +1,135 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 manifest_test
import (
"context"
"fmt"
"net/http"
"testing"
"time"
"github.com/distribution/distribution/v3/configuration"
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/spf13/cobra"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
"github.com/falcosecurity/falcoctl/cmd"
"github.com/falcosecurity/falcoctl/pkg/oci"
"github.com/falcosecurity/falcoctl/pkg/oci/authn"
ocipusher "github.com/falcosecurity/falcoctl/pkg/oci/pusher"
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
testutils "github.com/falcosecurity/falcoctl/pkg/test"
)
var (
localRegistryHost string
localRegistry *remote.Registry
testRuleTarball = "../../../pkg/test/data/rules.tar.gz"
testPluginTarball = "../../../pkg/test/data/plugin.tar.gz"
testPluginPlatform1 = "linux/amd64"
testPluginPlatform2 = "windows/amd64"
testPluginPlatform3 = "linux/arm64"
ctx = context.Background()
pluginMultiPlatformRef string
rulesRef string
output = gbytes.NewBuffer()
rootCmd *cobra.Command
opt *commonoptions.Common
)
func TestManifest(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Manifest Suite")
}
var _ = BeforeSuite(func() {
var err error
config := &configuration.Configuration{}
// Get a free port to be used by the registry.
port, err := testutils.FreePort()
Expect(err).ToNot(HaveOccurred())
// Create the registry address to which will bind.
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
localRegistryHost = config.HTTP.Addr
// Create the oras registry.
localRegistry, err = testutils.NewOrasRegistry(localRegistryHost, true)
Expect(err).ToNot(HaveOccurred())
// Start the local registry.
go func() {
err := testutils.StartRegistry(context.Background(), config)
Expect(err).ToNot(BeNil())
}()
// Check that the registry is up and accepting connections.
Eventually(func(g Gomega) error {
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
g.Expect(err).ShouldNot(HaveOccurred())
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
return err
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
// Initialize options for command.
opt = commonoptions.NewOptions()
opt.Initialize(commonoptions.WithWriter(output))
// Push the artifacts to the registry.
// Same artifacts will be used to test the puller code.
pusher := ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), true, nil)
// Push plugin artifact with multiple architectures.
filePathsAndPlatforms := ocipusher.WithFilepathsAndPlatforms([]string{testPluginTarball, testPluginTarball, testPluginTarball},
[]string{testPluginPlatform1, testPluginPlatform2, testPluginPlatform3})
pluginMultiPlatformRef = localRegistryHost + "/plugins:multiplatform"
artConfig := oci.ArtifactConfig{}
Expect(artConfig.ParseDependencies("my-dep:1.2.3|my-alt-dep:1.4.5")).ToNot(HaveOccurred())
Expect(artConfig.ParseRequirements("my-req:7.8.9")).ToNot(HaveOccurred())
artifactConfig := ocipusher.WithArtifactConfig(artConfig)
// Build options slice.
options := []ocipusher.Option{filePathsAndPlatforms, artifactConfig}
// Push the plugin artifact.
_, err = pusher.Push(ctx, oci.Plugin, pluginMultiPlatformRef, options...)
Expect(err).ShouldNot(HaveOccurred())
// Prepare and push artifact without config layer.
filePaths := ocipusher.WithFilepaths([]string{testRuleTarball})
artConfig = oci.ArtifactConfig{}
Expect(artConfig.ParseDependencies("dep1:1.2.3", "dep2:2.3.1")).ToNot(HaveOccurred())
options = []ocipusher.Option{
filePaths,
ocipusher.WithTags("latest"),
}
// Push a rulesfile artifact
options = append(options, ocipusher.WithArtifactConfig(artConfig))
rulesRef = localRegistryHost + "/rulesfiles:regular"
_, err = pusher.Push(ctx, oci.Rulesfile, rulesRef, options...)
Expect(err).ShouldNot(HaveOccurred())
})
func executeRoot(args []string) error {
rootCmd.SetArgs(args)
rootCmd.SetOut(output)
return cmd.Execute(rootCmd, opt)
}

View File

@ -0,0 +1,204 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 manifest_test
import (
"regexp"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/falcosecurity/falcoctl/cmd"
)
var usage = `Usage:
falcoctl artifact manifest [ref] [flags]
Flags:
-h, --help help for manifest
--plain-http allows interacting with remote registry via plain http requests
--platform string os and architecture of the artifact in OS/ARCH format (default "linux/amd64")
Global Flags:
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
--log-format string Set formatting for logs (color, text, json) (default "color")
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
`
var help = `Get the manifest layer of an artifact
Usage:
falcoctl artifact manifest [ref] [flags]
Flags:
-h, --help help for manifest
--plain-http allows interacting with remote registry via plain http requests
--platform string os and architecture of the artifact in OS/ARCH format (default "linux/amd64")
Global Flags:
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
--log-format string Set formatting for logs (color, text, json) (default "color")
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
`
var _ = Describe("Manifest", func() {
const (
artifactCmd = "artifact"
manifestCmd = "manifest"
plaingHTTP = "--plain-http"
configFlag = "--config"
platformFlag = "--platform"
)
var (
err error
args []string
configDir string
)
var assertFailedBehavior = func(usage, specificError string) {
It("check that fails and the usage is not printed", func() {
Expect(err).To(HaveOccurred())
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
})
}
JustBeforeEach(func() {
configDir = GinkgoT().TempDir()
rootCmd = cmd.New(ctx, opt)
err = executeRoot(args)
})
JustAfterEach(func() {
err = nil
Expect(output.Clear()).ShouldNot(HaveOccurred())
args = nil
})
Context("help message", func() {
BeforeEach(func() {
args = []string{artifactCmd, manifestCmd, "--help"}
})
It("should match the saved one", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(string(output.Contents())).Should(Equal(help))
})
})
Context("wrong number of arguments", func() {
When("number of arguments equal to 0", func() {
BeforeEach(func() {
args = []string{artifactCmd, manifestCmd}
})
assertFailedBehavior(usage, "ERROR accepts 1 arg(s), received 0 ")
})
When("number of arguments equal to 2", func() {
BeforeEach(func() {
args = []string{artifactCmd, manifestCmd, "arg1", "arg2", configFlag, configDir}
})
assertFailedBehavior(usage, "ERROR accepts 1 arg(s), received 2 ")
})
})
Context("failure", func() {
When("unreachable/non existing registry", func() {
BeforeEach(func() {
args = []string{artifactCmd, manifestCmd, "noregistry/noartifact", plaingHTTP, configFlag, configDir}
})
assertFailedBehavior(usage, "ERROR unable to fetch reference \"noregistry/noartifact:latest\"")
})
When("non existing repository", func() {
BeforeEach(func() {
args = []string{artifactCmd, manifestCmd, localRegistryHost + "/noartifact", plaingHTTP, configFlag, configDir}
})
assertFailedBehavior(usage, "noartifact:latest: not found")
})
When("non parsable reference", func() {
BeforeEach(func() {
args = []string{artifactCmd, manifestCmd, " ", plaingHTTP, configFlag, configDir}
})
assertFailedBehavior(usage, "ERROR cannot find among the configured indexes, skipping ")
})
When("no manifest for given platform", func() {
BeforeEach(func() {
args = []string{artifactCmd, manifestCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir, platformFlag, "linux/wrong"}
})
assertFailedBehavior(usage, "ERROR unable to find a manifest matching the given platform: linux/wrong")
})
})
Context("success", func() {
When("without image index and no platform (rulesfiles)", func() {
BeforeEach(func() {
args = []string{artifactCmd, manifestCmd, rulesRef, plaingHTTP, configFlag, configDir}
})
It("should success", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.cncf.falco.rulesfile.config.v1+json","digest":"sha256:c329db306d80e7f1e3a5df28bb7d75a0a1545ad1e8f717a4ab4534a3d558affa","size":86},"layers":[{"mediaType":"application/vnd.cncf.falco.rulesfile.layer.v1+tar.gz","digest":"sha256:8ed676f9801d987a26854827beb176eb9164dec3b09a714406348fe1096f7c6c","size":2560,"annotations":{"org.opencontainers.image.title":"rules.tar.gz"}}],"annotations":{"org.opencontainers.image.created":`))) //nolint:lll //testing purpose
})
})
When("no platform flag", func() {
BeforeEach(func() {
args = []string{artifactCmd, manifestCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir}
})
It("should success getting the platform where tests are running", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.cncf.falco.plugin.config.v1+json","digest":"sha256:39ae8c14fd9ef38d0f1836ba7be71627023ce615f165c3663586a325eee04724","size":164},"layers":[{"mediaType":"application/vnd.cncf.falco.plugin.layer.v1+tar.gz","digest":"sha256:45a192b10e9bbfc82f4216b071afefd7fba56e02e856e37186430d40160e5d64","size":6659921,"annotations":{"org.opencontainers.image.title":"plugin.tar.gz"}}],"annotations":{"org.opencontainers.image.created":`))) //nolint:lll //testing purpose
})
})
When("with valid platform", func() {
BeforeEach(func() {
args = []string{artifactCmd, manifestCmd, pluginMultiPlatformRef, plaingHTTP, configFlag, configDir, platformFlag, testPluginPlatform3}
})
It("should success", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.cncf.falco.plugin.config.v1+json","digest":"sha256:39ae8c14fd9ef38d0f1836ba7be71627023ce615f165c3663586a325eee04724","size":164},"layers":[{"mediaType":"application/vnd.cncf.falco.plugin.layer.v1+tar.gz","digest":"sha256:45a192b10e9bbfc82f4216b071afefd7fba56e02e856e37186430d40160e5d64","size":6659921,"annotations":{"org.opencontainers.image.title":"plugin.tar.gz"}}],"annotations":{"org.opencontainers.image.created":`))) //nolint:lll //testing purpose
})
})
When("with non existing platform for artifacts without platforms", func() {
BeforeEach(func() {
args = []string{artifactCmd, manifestCmd, rulesRef, plaingHTTP, configFlag, configDir, platformFlag, testPluginPlatform3}
})
It("should success and ignore the platform flag", func() {
Expect(err).ShouldNot(HaveOccurred())
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.cncf.falco.rulesfile.config.v1+json","digest":"sha256:c329db306d80e7f1e3a5df28bb7d75a0a1545ad1e8f717a4ab4534a3d558affa","size":86},"layers":[{"mediaType":"application/vnd.cncf.falco.rulesfile.layer.v1+tar.gz","digest":"sha256:8ed676f9801d987a26854827beb176eb9164dec3b09a714406348fe1096f7c6c","size":2560,"annotations":{"org.opencontainers.image.title":"rules.tar.gz"}}],"annotations":{"org.opencontainers.image.created":`))) //nolint:lll //testing purpose
})
})
})
})

View File

@ -0,0 +1,91 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 search
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/falcosecurity/falcoctl/pkg/oci"
"github.com/falcosecurity/falcoctl/pkg/options"
"github.com/falcosecurity/falcoctl/pkg/output"
)
const (
defaultMinScore = 0.65
// CommandName name of the command. It has to be the first word in the use line.
CommandName = "search"
)
type artifactSearchOptions struct {
*options.Common
minScore float64
artifactType oci.ArtifactType
}
func (o *artifactSearchOptions) Validate() error {
if o.minScore <= 0 || o.minScore > 1 {
return fmt.Errorf("minScore must be a number within (0,1]")
}
return nil
}
// NewArtifactSearchCmd returns the artifact search command.
func NewArtifactSearchCmd(ctx context.Context, opt *options.Common) *cobra.Command {
o := artifactSearchOptions{
Common: opt,
}
cmd := &cobra.Command{
Use: fmt.Sprintf("%s [keyword1 [keyword2 ...]] [flags]", CommandName),
DisableFlagsInUseLine: true,
Short: "Search an artifact by keywords",
Long: "Search an artifact by keywords",
Args: cobra.MinimumNArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
return o.Validate()
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunArtifactSearch(ctx, args)
},
}
cmd.Flags().Float64VarP(&o.minScore, "min-score", "", defaultMinScore,
"the minimum score used to match artifact names with search keywords")
cmd.Flags().Var(&o.artifactType, "type", `Only search artifacts with a specific type. Allowed values: "rulesfile", "plugin", "asset"`)
return cmd
}
func (o *artifactSearchOptions) RunArtifactSearch(_ context.Context, args []string) error {
resultEntries := o.IndexCache.MergedIndexes.SearchByKeywords(o.minScore, args...)
var data [][]string
for _, entry := range resultEntries {
if o.artifactType != "" && o.artifactType != oci.ArtifactType(entry.Type) {
continue
}
indexName := o.IndexCache.MergedIndexes.IndexByEntry(entry).Name
row := []string{indexName, entry.Name, entry.Type, entry.Registry, entry.Repository}
data = append(data, row)
}
return o.Printer.PrintTable(output.ArtifactSearch, data)
}

View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 search defines the logic to search for artifacts in the configured index files.
package search

28
cmd/cmd_suite_test.go Normal file
View File

@ -0,0 +1,28 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestCmd(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Cmd Suite")
}

View File

@ -1,46 +0,0 @@
/*
Copyright © 2019 Kris Nova <kris@nivenly.com>
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
import (
"fmt"
"github.com/spf13/cobra"
)
// deleteCmd represents the delete command
var deleteCmd = &cobra.Command{
Use: "delete",
Short: "Delete a resource",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("delete called")
},
}
func init() {
rootCmd.AddCommand(deleteCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// deleteCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// deleteCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View File

@ -1,73 +0,0 @@
/*
Copyright © 2019 Kris Nova <kris@nivenly.com>
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
import (
"fmt"
"os"
kubernetesfalc "github.com/kris-nova/falcoctl/kubernetes"
"github.com/kubicorn/kubicorn/pkg/cli"
"github.com/kubicorn/kubicorn/pkg/local"
"github.com/kubicorn/kubicorn/pkg/logger"
"github.com/spf13/cobra"
)
// deleteFalcoCmd represents the deleteFalco command
var (
deleteFalcoCmd = &cobra.Command{
Use: "falco",
Short: "Delete Falco from Kubernetes",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
err, exitCode := DeleteFalcoEntry(i, kubeConfigPath)
if err != nil {
logger.Critical("Fatal error: %v", err)
os.Exit(exitCode)
}
logger.Always("Success.")
/// os.Exit(1) },
},
}
)
var (
// Global for all install methods
i = &kubernetesfalc.FalcoInstaller{}
kubeConfigPath string
)
func init() {
deleteCmd.AddCommand(deleteFalcoCmd)
installFalcoCmd.Flags().StringVarP(&kubeConfigPath, "kube-config-path", "k",
cli.StrEnvDef("FALCOCTL_KUBE_CONFIG_PATH", fmt.Sprintf("%s/.kube/config", local.Home())),
"Set the path to the Kube config")
installFalcoCmd.Flags().StringVarP(&i.NamespaceName, "namespace", "n",
cli.StrEnvDef("FALCOCTL_KUBE_NAMESPACE", "falco"), "Set the namespace to install Falco in")
}
func DeleteFalcoEntry(installer *kubernetesfalc.FalcoInstaller, kubeConfigPath string) (error, int) {
k8s, err := kubernetesfalc.NewK8sFromKubeConfigPath(kubeConfigPath)
if err != nil {
return fmt.Errorf("unable to parse kube config: %v", err), 98
}
err = installer.Delete(k8s)
if err != nil {
return fmt.Errorf("unable to delete falco in Kubernetes: %v", err), 99
}
return nil, 0
}

17
cmd/doc.go Normal file
View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 implements all the falcoctl commands.
package cmd

View File

@ -0,0 +1,74 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 drivercleanup
import (
"bytes"
"strings"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"golang.org/x/net/context"
"github.com/falcosecurity/falcoctl/pkg/options"
)
type driverCleanupOptions struct {
*options.Common
*options.Driver
}
// NewDriverCleanupCmd cleans a driver up.
func NewDriverCleanupCmd(ctx context.Context, opt *options.Common, driver *options.Driver) *cobra.Command {
o := driverCleanupOptions{
Common: opt,
Driver: driver,
}
cmd := &cobra.Command{
Use: "cleanup [flags]",
DisableFlagsInUseLine: true,
Short: "Cleanup a driver",
Long: `Cleans a driver up, eg for kmod, by removing it from dkms.`,
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunDriverCleanup(ctx)
},
}
return cmd
}
func (o *driverCleanupOptions) RunDriverCleanup(_ context.Context) error {
o.Printer.Logger.Info("Running falcoctl driver cleanup", o.Printer.Logger.Args(
"driver type", o.Driver.Type,
"driver name", o.Driver.Name))
var buf bytes.Buffer
if !o.Printer.DisableStyling {
o.Printer.Spinner, _ = o.Printer.Spinner.Start("Cleaning up existing drivers")
}
err := o.Driver.Type.Cleanup(o.Printer.WithWriter(&buf), o.Driver.Name)
if o.Printer.Spinner != nil {
_ = o.Printer.Spinner.Stop()
}
if o.Printer.Logger.Formatter == pterm.LogFormatterJSON {
// Only print formatted text if we are formatting to json
out := strings.ReplaceAll(buf.String(), "\n", ";")
o.Printer.Logger.Info("Driver cleanup", o.Printer.Logger.Args("output", out))
} else {
// Print much more readable output as-is
o.Printer.DefaultText.Print(buf.String())
}
return err
}

View File

@ -0,0 +1,69 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 drivercleanup_test
import (
"context"
"os"
"path/filepath"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/spf13/cobra"
"github.com/falcosecurity/falcoctl/cmd"
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
testutils "github.com/falcosecurity/falcoctl/pkg/test"
)
var (
ctx = context.Background()
output = gbytes.NewBuffer()
rootCmd *cobra.Command
opt *commonoptions.Common
configFile string
err error
args []string
)
func TestCleanup(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Cleanup Suite")
}
var _ = BeforeSuite(func() {
// Create and configure the common options.
opt = commonoptions.NewOptions()
opt.Initialize(commonoptions.WithWriter(output))
// Create temporary directory used to save the configuration file.
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
Expect(err).Should(Succeed())
})
var _ = AfterSuite(func() {
configDir := filepath.Dir(configFile)
Expect(os.RemoveAll(configDir)).Should(Succeed())
})
func executeRoot(args []string) error {
rootCmd.SetArgs(args)
rootCmd.SetOut(output)
return cmd.Execute(rootCmd, opt)
}

View File

@ -0,0 +1,101 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2024 The Falco Authors
//
// 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 drivercleanup_test
import (
"regexp"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/falcosecurity/falcoctl/cmd"
)
//nolint:lll // no need to check for line length.
var driverCleanupHelp = `Cleans a driver up, eg for kmod, by removing it from dkms.
Usage:
falcoctl driver cleanup [flags]
Flags:
-h, --help help for cleanup
Global Flags:
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
--host-root string Driver host root to be used. (default "/")
--kernelrelease string Specify the kernel release for which to download/build the driver in the same format used by 'uname -r' (e.g. '6.1.0-10-cloud-amd64')
--kernelversion string Specify the kernel version for which to download/build the driver in the same format used by 'uname -v' (e.g. '#1 SMP PREEMPT_DYNAMIC Debian 6.1.38-2 (2023-07-27)')
--log-format string Set formatting for logs (color, text, json) (default "color")
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
--name string Driver name to be used. (default "falco")
--repo strings Driver repo to be used. (default [https://download.falco.org/driver])
--type strings Driver types allowed in descending priority order (ebpf, kmod, modern_ebpf) (default [modern_ebpf,kmod,ebpf])
--version string Driver version to be used.
`
var addAssertFailedBehavior = func(specificError string) {
It("check that fails and the usage is not printed", func() {
Expect(err).To(HaveOccurred())
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
})
}
var _ = Describe("cleanup", func() {
var (
driverCmd = "driver"
cleanupCmd = "cleanup"
)
// Each test gets its own root command and runs it.
// The err variable is asserted by each test.
JustBeforeEach(func() {
rootCmd = cmd.New(ctx, opt)
err = executeRoot(args)
})
JustAfterEach(func() {
Expect(output.Clear()).ShouldNot(HaveOccurred())
})
Context("help message", func() {
BeforeEach(func() {
args = []string{driverCmd, cleanupCmd, "--help"}
})
It("should match the saved one", func() {
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(driverCleanupHelp)))
})
})
// Here we are testing failure cases for cleaning a driver.
Context("failure", func() {
When("with non absolute host-root", func() {
BeforeEach(func() {
args = []string{driverCmd, cleanupCmd, "--config", configFile, "--host-root", "foo/"}
})
addAssertFailedBehavior("ERROR host-root must be an absolute path (foo/)")
})
When("with invalid driver type", func() {
BeforeEach(func() {
args = []string{driverCmd, cleanupCmd, "--config", configFile, "--type", "foo"}
})
addAssertFailedBehavior(`ERROR unsupported driver type specified: foo`)
})
})
})

17
cmd/driver/cleanup/doc.go Normal file
View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 drivercleanup defines the cleanup logic for the driver cmd.
package drivercleanup

View File

@ -0,0 +1,326 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2024 The Falco Authors
//
// 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 driverconfig
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/falcosecurity/driverkit/pkg/kernelrelease"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
drivertype "github.com/falcosecurity/falcoctl/pkg/driver/type"
"github.com/falcosecurity/falcoctl/pkg/options"
)
const (
falcoName = "falco"
)
func newOptions() *driverConfigOptions {
common := options.NewOptions()
common.Initialize()
// Parse the driver type.
dType, _ := drivertype.Parse("modern_ebpf")
return &driverConfigOptions{
Common: common,
Driver: &options.Driver{
Type: dType,
Name: falcoName,
Repos: []string{"https://download.falco.org/driver"},
Version: "6.0.0+driver",
HostRoot: "/",
Distro: nil,
Kr: kernelrelease.KernelRelease{},
},
update: false,
namespace: "",
kubeconfig: "",
configmap: "",
configDir: "",
}
}
func createFalcoConfigFile(cfg falcoCfg, configDir string) error {
engineKind, err := yaml.Marshal(cfg)
if err != nil {
return fmt.Errorf("unable to marshal falco config: %w", err)
}
// Write the engine configuration to a specialized config file.
if err := os.WriteFile(filepath.Join(configDir, "falco.yaml"), engineKind, 0o600); err != nil {
return fmt.Errorf("unable to write falco.yaml file: %w", err)
}
return nil
}
func createFalcoConfigMap(cfg falcoCfg, dataKey string) (*v1.ConfigMap, error) {
engineKind, err := yaml.Marshal(cfg)
if err != nil {
return nil, fmt.Errorf("unable to marshal falco config: %w", err)
}
cm := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: falcoName,
Namespace: falcoName,
},
Data: map[string]string{
dataKey: string(engineKind),
},
}
return cm, nil
}
func TestDriverConfigOptions_Commit_Host(t *testing.T) {
testCases := []struct {
name string
args func(t *testing.T) *driverConfigOptions
expected func(t *testing.T, opt *driverConfigOptions, err error)
}{
{
"no falco config file",
func(t *testing.T) *driverConfigOptions {
opt := newOptions()
opt.configDir = "no-file-at-all"
opt.update = true
return opt
},
func(t *testing.T, opt *driverConfigOptions, err error) {
require.Error(t, err, "should error since falco configuration file does not exist")
require.ErrorContains(t, err, "open no-file-at-all/falco.yaml: no such file or directory")
},
},
{
"update-falco-config",
func(t *testing.T) *driverConfigOptions {
opt := newOptions()
dir, err := os.MkdirTemp("", "falcoctl-driver-config-test")
require.NoError(t, err)
// Write falco configuration file.
cfg := falcoCfg{engineCfg{Kind: "modern_ebpf"}}
err = createFalcoConfigFile(cfg, dir)
require.NoError(t, err)
opt.configDir = dir
return opt
},
func(t *testing.T, opt *driverConfigOptions, err error) {
require.NoError(t, err, "should not error")
// Config file.
specCfgFile := filepath.Join(opt.configDir, "config.d", falcoDriverConfigFile)
// Check that config file has been created.
_, err = os.Stat(specCfgFile)
require.NoError(t, err)
content, err := os.ReadFile(specCfgFile)
require.NoError(t, err)
cfg := falcoCfg{}
err = yaml.Unmarshal(content, &cfg)
require.NoError(t, err)
require.Equal(t, opt.Type.String(), cfg.Engine.Kind)
},
},
{
"falco-not-in-driver-mode",
func(t *testing.T) *driverConfigOptions {
opt := newOptions()
dir, err := os.MkdirTemp("", "falcoctl-driver-config-test")
require.NoError(t, err)
// Write falco configuration file.
cfg := falcoCfg{engineCfg{Kind: "nodriver"}}
err = createFalcoConfigFile(cfg, dir)
require.NoError(t, err)
opt.configDir = dir
return opt
},
func(t *testing.T, opt *driverConfigOptions, err error) {
require.NoError(t, err, "should not error")
// Config file.
specCfgFile := filepath.Join(opt.configDir, "config.d", falcoDriverConfigFile)
// Check that config file has been created.
_, err = os.Stat(specCfgFile)
require.True(t, os.IsNotExist(err))
},
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
opt := testCase.args(t)
err := opt.Commit(context.Background(), nil, opt.Type)
testCase.expected(t, opt, err)
})
}
}
func TestDriverConfigOptions_Commit_K8S(t *testing.T) {
testCases := []struct {
name string
args func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap)
expected func(t *testing.T, opt *driverConfigOptions, err error)
}{
{
"no falco configmap, wrong namespace",
func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap) {
opt := newOptions()
opt.namespace = "wrong-namespace"
opt.configmap = falcoName
cm, err := createFalcoConfigMap(falcoCfg{engineCfg{Kind: "modern_ebpf"}}, "falco.yaml")
require.NoError(t, err)
return opt, cm
},
func(t *testing.T, opt *driverConfigOptions, err error) {
require.Error(t, err, "should error since falco configmap does not exist")
require.ErrorContains(t, err, "unable to get configmap falco in namespace wrong-namespace")
},
},
{
"no falco configmap, wrong name",
func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap) {
opt := newOptions()
opt.namespace = falcoName
opt.configmap = "wrong-name"
cm, err := createFalcoConfigMap(falcoCfg{engineCfg{Kind: "modern_ebpf"}}, "falco.yaml")
require.NoError(t, err)
return opt, cm
},
func(t *testing.T, opt *driverConfigOptions, err error) {
require.Error(t, err, "should error since falco configmap does not exist")
require.ErrorContains(t, err, "unable to get configmap wrong-name in namespace falco")
},
},
{
"no falco config, wrong data key",
func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap) {
opt := newOptions()
opt.namespace = falcoName
opt.configmap = falcoName
cm, err := createFalcoConfigMap(falcoCfg{engineCfg{Kind: "modern_ebpf"}}, "wrong-data-key")
require.NoError(t, err)
return opt, cm
},
func(t *testing.T, opt *driverConfigOptions, err error) {
require.Error(t, err, "should error since falco configmap does not exist")
require.ErrorContains(t, err, "configMap falco does not contain key \"falco.yaml\"")
},
},
{
"update-falco-config",
func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap) {
opt := newOptions()
opt.namespace = falcoName
opt.configmap = falcoName
dir, err := os.MkdirTemp("", "falcoctl-driver-config-test")
require.NoError(t, err)
opt.configDir = dir
cm, err := createFalcoConfigMap(falcoCfg{engineCfg{Kind: "modern_ebpf"}}, "falco.yaml")
require.NoError(t, err)
return opt, cm
},
func(t *testing.T, opt *driverConfigOptions, err error) {
require.NoError(t, err, "should not error")
// Config file.
specCfgFile := filepath.Join(opt.configDir, "config.d", falcoDriverConfigFile)
// Check that config file has been created.
_, err = os.Stat(specCfgFile)
require.NoError(t, err)
content, err := os.ReadFile(specCfgFile)
require.NoError(t, err)
cfg := falcoCfg{}
err = yaml.Unmarshal(content, &cfg)
require.NoError(t, err)
require.Equal(t, opt.Type.String(), cfg.Engine.Kind)
},
},
{
"falco-not-in-driver-mode",
func(t *testing.T) (*driverConfigOptions, *v1.ConfigMap) {
opt := newOptions()
opt.namespace = falcoName
opt.configmap = falcoName
dir, err := os.MkdirTemp("", "falcoctl-driver-config-test")
require.NoError(t, err)
cm, err := createFalcoConfigMap(falcoCfg{engineCfg{Kind: "nodriver"}}, "falco.yaml")
require.NoError(t, err)
opt.configDir = dir
return opt, cm
},
func(t *testing.T, opt *driverConfigOptions, err error) {
require.NoError(t, err, "should not error")
// Config file.
specCfgFile := filepath.Join(opt.configDir, "config.d", falcoDriverConfigFile)
// Check that config file has been created.
_, err = os.Stat(specCfgFile)
require.True(t, os.IsNotExist(err))
},
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
opt, cm := testCase.args(t)
// Create fake client.
fakeClient := fake.NewSimpleClientset(cm)
err := opt.Commit(context.Background(), fakeClient, opt.Type)
testCase.expected(t, opt, err)
})
}
}

262
cmd/driver/config/config.go Normal file
View File

@ -0,0 +1,262 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2024 The Falco Authors
//
// 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 driverconfig
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/net/context"
"gopkg.in/yaml.v3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"github.com/falcosecurity/falcoctl/internal/config"
drivertype "github.com/falcosecurity/falcoctl/pkg/driver/type"
"github.com/falcosecurity/falcoctl/pkg/options"
)
const (
longConfig = `Configure a driver for future usages with other driver subcommands.
It will also update local Falco configuration or k8s configmap depending on the environment where it is running, to let Falco use chosen driver.
Only supports deployments of Falco that use a driver engine, ie: one between kmod, ebpf and modern-ebpf.
If engine.kind key is set to a non-driver driven engine, Falco configuration won't be touched.
`
falcoConfigFile = "falco.yaml"
falcoDriverConfigFile = "engine-kind-falcoctl.yaml"
)
type driverConfigOptions struct {
*options.Common
*options.Driver
update bool
namespace string
kubeconfig string
configmap string
configDir string
}
type engineCfg struct {
Kind string `yaml:"kind"`
}
type falcoCfg struct {
Engine engineCfg `yaml:"engine"`
}
// NewDriverConfigCmd configures a driver and stores it in config.
func NewDriverConfigCmd(ctx context.Context, opt *options.Common, driver *options.Driver) *cobra.Command {
o := driverConfigOptions{
Common: opt,
Driver: driver,
}
cmd := &cobra.Command{
Use: "config [flags]",
DisableFlagsInUseLine: true,
Short: "Configure a driver",
Long: longConfig,
PreRunE: func(cmd *cobra.Command, args []string) error {
viper.AutomaticEnv()
_ = viper.BindPFlag("driver.config.configmap", cmd.Flags().Lookup("configmap"))
_ = viper.BindPFlag("driver.config.namespace", cmd.Flags().Lookup("namespace"))
_ = viper.BindPFlag("driver.config.update_falco", cmd.Flags().Lookup("update-falco"))
_ = viper.BindPFlag("driver.config.kubeconfig", cmd.Flags().Lookup("kubeconfig"))
_ = viper.BindPFlag("driver.config.configdir", cmd.Flags().Lookup("falco-config-dir"))
o.configmap = viper.GetString("driver.config.configmap")
o.namespace = viper.GetString("driver.config.namespace")
o.kubeconfig = viper.GetString("driver.config.kubeconfig")
o.update = viper.GetBool("driver.config.update_falco")
o.configDir = viper.GetString("driver.config.configdir")
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunDriverConfig(ctx)
},
}
cmd.Flags().BoolVar(&o.update, "update-falco", true, "Whether to overwrite Falco configuration")
cmd.Flags().StringVar(&o.namespace, "namespace", "", "Kubernetes namespace.")
cmd.Flags().StringVar(&o.kubeconfig, "kubeconfig", "", "Kubernetes config.")
cmd.Flags().StringVar(&o.configmap, "configmap", "", "Falco configmap name.")
cmd.Flags().StringVar(&o.configDir, "falco-config-dir", "/etc/falco", "Falco configuration directory.")
return cmd
}
// RunDriverConfig implements the driver configuration command.
func (o *driverConfigOptions) RunDriverConfig(ctx context.Context) error {
o.Printer.Logger.Info("Running falcoctl driver config", o.Printer.Logger.Args(
"name", o.Driver.Name,
"version", o.Driver.Version,
"type", o.Driver.Type.String(),
"host-root", o.Driver.HostRoot,
"repos", strings.Join(o.Driver.Repos, ",")))
if o.update {
var cl kubernetes.Interface
var err error
if o.namespace != "" {
// Create a new clientset.
if cl, err = setupClient(o.kubeconfig); err != nil {
return err
}
}
if err := o.Commit(ctx, cl, o.Driver.Type); err != nil {
return err
}
}
o.Printer.Logger.Info("Storing falcoctl driver config")
return config.StoreDriver(o.Driver.ToDriverConfig(), o.ConfigFile)
}
func checkFalcoRunsWithDrivers(engineKind string) bool {
// Modify the data in the ConfigMap/Falco config file ONLY if engine.kind is set to a known driver type.
// This ensures that we modify the config only for Falcos running with drivers, and not plugins/gvisor.
// Scenario: user has multiple Falco pods deployed in its cluster, one running with driver,
// other running with plugins. We must only touch the one running with driver.
if _, err := drivertype.Parse(engineKind); err != nil {
return false
}
return true
}
func (o *driverConfigOptions) IsRunningInDriverModeHost() (bool, error) {
o.Printer.Logger.Debug("Checking if Falco is running in driver mode on host system")
falcoCfgFile := filepath.Join(o.configDir, falcoConfigFile)
yamlFile, err := os.ReadFile(filepath.Clean(falcoCfgFile))
if err != nil {
return false, err
}
cfg := falcoCfg{}
if err = yaml.Unmarshal(yamlFile, &cfg); err != nil {
return false, fmt.Errorf("unable to unmarshal falco.yaml to falcoCfg struct: %w", err)
}
return checkFalcoRunsWithDrivers(cfg.Engine.Kind), nil
}
func (o *driverConfigOptions) IsRunningInDriverModeK8S(ctx context.Context, cl kubernetes.Interface) (bool, error) {
o.Printer.Logger.Debug("Checking if Falco is running in driver mode in Kubernetes")
configMap, err := cl.CoreV1().ConfigMaps(o.namespace).Get(ctx, o.configmap, metav1.GetOptions{})
if err != nil {
return false, fmt.Errorf("unable to get configmap %s in namespace %s: %w", o.configmap, o.namespace, err)
}
// Check that this is a Falco config map
falcoYaml, present := configMap.Data["falco.yaml"]
if !present {
o.Printer.Logger.Debug("Skip non Falco-related config map",
o.Printer.Logger.Args("configMap", configMap.Name))
return false, fmt.Errorf("configMap %s does not contain key \"falco.yaml\"", o.configmap)
}
// Check that Falco is configured to run with a driver
var falcoConfig falcoCfg
err = yaml.Unmarshal([]byte(falcoYaml), &falcoConfig)
if err != nil {
return false, fmt.Errorf("unable to unmarshal falco.yaml to falcoCfg struct: %w", err)
}
return checkFalcoRunsWithDrivers(falcoConfig.Engine.Kind), nil
}
// Commit saves the updated driver type to Falco config,
// in a specialized configuration file under /etc/falco/config.d.
func (o *driverConfigOptions) Commit(ctx context.Context, cl kubernetes.Interface, driverType drivertype.DriverType) error {
// If set to true, then we need to overwrite the driver type.
var overwrite bool
var err error
if cl != nil {
if overwrite, err = o.IsRunningInDriverModeK8S(ctx, cl); err != nil {
return err
}
} else {
if overwrite, err = o.IsRunningInDriverModeHost(); err != nil {
return err
}
}
if overwrite {
o.Printer.Logger.Info("Committing driver config to specialized configuration file under",
o.Printer.Logger.Args("directory", filepath.Join(o.configDir, "config.d")))
return overwriteDriverType(o.configDir, driverType)
}
o.Printer.Logger.Info("Falco is not configured to run with a driver, no need to set driver type.")
return nil
}
func setupClient(kubeconfig string) (kubernetes.Interface, error) {
var cfg *rest.Config
var err error
// Create the rest config.
if kubeconfig != "" {
cfg, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
} else {
cfg, err = rest.InClusterConfig()
}
if err != nil {
return nil, err
}
// Create the clientset.
return kubernetes.NewForConfig(cfg)
}
func overwriteDriverType(configDir string, driverType drivertype.DriverType) error {
var falcoConfig falcoCfg
configDir = filepath.Join(configDir, "config.d")
// First thing, check if config.d folder exists in the configuration directory.
_, err := os.Stat(configDir)
if os.IsNotExist(err) {
// Create it.
// #nosec G301 -- under /etc we want 755 permissions
if err := os.MkdirAll(configDir, 0o755); err != nil {
return fmt.Errorf("unable to create directory %s: %w", configDir, err)
}
} else if err != nil && !os.IsNotExist(err) {
return err
}
falcoConfig.Engine.Kind = driverType.String()
engineKind, err := yaml.Marshal(falcoConfig)
if err != nil {
return fmt.Errorf("unable to marshal falco config: %w", err)
}
// Write the engine configuration to a specialized config file.
// #nosec G306 //under /etc we want 644 permissions
if err := os.WriteFile(filepath.Join(configDir, falcoDriverConfigFile), engineKind, 0o644); err != nil {
return fmt.Errorf("unable to persist engine kind to filesystem: %w", err)
}
return nil
}

View File

@ -0,0 +1,69 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 driverconfig_test
import (
"context"
"os"
"path/filepath"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/spf13/cobra"
"github.com/falcosecurity/falcoctl/cmd"
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
testutils "github.com/falcosecurity/falcoctl/pkg/test"
)
var (
ctx = context.Background()
output = gbytes.NewBuffer()
rootCmd *cobra.Command
opt *commonoptions.Common
configFile string
err error
args []string
)
func TestConfig(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Config Suite")
}
var _ = BeforeSuite(func() {
// Create and configure the common options.
opt = commonoptions.NewOptions()
opt.Initialize(commonoptions.WithWriter(output))
// Create temporary directory used to save the configuration file.
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
Expect(err).Should(Succeed())
})
var _ = AfterSuite(func() {
configDir := filepath.Dir(configFile)
Expect(os.RemoveAll(configDir)).Should(Succeed())
})
func executeRoot(args []string) error {
rootCmd.SetArgs(args)
rootCmd.SetOut(output)
return cmd.Execute(rootCmd, opt)
}

View File

@ -0,0 +1,109 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2024 The Falco Authors
//
// 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 driverconfig_test
import (
"regexp"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/falcosecurity/falcoctl/cmd"
)
//nolint:lll // no need to check for line length.
var driverConfigHelp = `Configure a driver for future usages with other driver subcommands.
It will also update local Falco configuration or k8s configmap depending on the environment where it is running, to let Falco use chosen driver.
Only supports deployments of Falco that use a driver engine, ie: one between kmod, ebpf and modern-ebpf.
If engine.kind key is set to a non-driver driven engine, Falco configuration won't be touched.
Usage:
falcoctl driver config [flags]
Flags:
--configmap string Falco configmap name.
--falco-config-dir string Falco configuration directory. (default "/etc/falco")
-h, --help help for config
--kubeconfig string Kubernetes config.
--namespace string Kubernetes namespace.
--update-falco Whether to overwrite Falco configuration (default true)
Global Flags:
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
--host-root string Driver host root to be used. (default "/")
--kernelrelease string Specify the kernel release for which to download/build the driver in the same format used by 'uname -r' (e.g. '6.1.0-10-cloud-amd64')
--kernelversion string Specify the kernel version for which to download/build the driver in the same format used by 'uname -v' (e.g. '#1 SMP PREEMPT_DYNAMIC Debian 6.1.38-2 (2023-07-27)')
--log-format string Set formatting for logs (color, text, json) (default "color")
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
--name string Driver name to be used. (default "falco")
--repo strings Driver repo to be used. (default [https://download.falco.org/driver])
--type strings Driver types allowed in descending priority order (ebpf, kmod, modern_ebpf) (default [modern_ebpf,kmod,ebpf])
--version string Driver version to be used.
`
var addAssertFailedBehavior = func(specificError string) {
It("check that fails and the usage is not printed", func() {
Expect(err).To(HaveOccurred())
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
})
}
var _ = Describe("config", func() {
var (
driverCmd = "driver"
configCmd = "config"
)
// Each test gets its own root command and runs it.
// The err variable is asserted by each test.
JustBeforeEach(func() {
rootCmd = cmd.New(ctx, opt)
err = executeRoot(args)
})
JustAfterEach(func() {
Expect(output.Clear()).ShouldNot(HaveOccurred())
})
Context("help message", func() {
BeforeEach(func() {
args = []string{driverCmd, configCmd, "--help"}
})
It("should match the saved one", func() {
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(driverConfigHelp)))
})
})
// Here we are testing failure cases for configuring a driver.
Context("failure", func() {
When("with non absolute host-root", func() {
BeforeEach(func() {
args = []string{driverCmd, configCmd, "--config", configFile, "--host-root", "foo/"}
})
addAssertFailedBehavior("ERROR host-root must be an absolute path (foo/)")
})
When("with invalid driver type", func() {
BeforeEach(func() {
args = []string{driverCmd, configCmd, "--config", configFile, "--type", "foo"}
})
addAssertFailedBehavior(`ERROR unsupported driver type specified: foo`)
})
})
})

17
cmd/driver/config/doc.go Normal file
View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 driverconfig defines the configure logic for the driver cmd.
package driverconfig

241
cmd/driver/driver_linux.go Normal file
View File

@ -0,0 +1,241 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2024 The Falco Authors
//
// 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.
//go:build linux
// Package driver implements the driver related cmd line interface.
package driver
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/blang/semver"
"github.com/spf13/cobra"
"github.com/spf13/viper"
drivercleanup "github.com/falcosecurity/falcoctl/cmd/driver/cleanup"
driverconfig "github.com/falcosecurity/falcoctl/cmd/driver/config"
driverinstall "github.com/falcosecurity/falcoctl/cmd/driver/install"
driverprintenv "github.com/falcosecurity/falcoctl/cmd/driver/printenv"
"github.com/falcosecurity/falcoctl/internal/config"
driverdistro "github.com/falcosecurity/falcoctl/pkg/driver/distro"
driverkernel "github.com/falcosecurity/falcoctl/pkg/driver/kernel"
drivertype "github.com/falcosecurity/falcoctl/pkg/driver/type"
"github.com/falcosecurity/falcoctl/pkg/options"
)
// NewDriverCmd returns the driver command.
func NewDriverCmd(ctx context.Context, opt *options.Common) *cobra.Command {
driver := &options.Driver{}
driverTypesEnum := options.NewDriverTypes()
var (
driverTypesStr []string
driverKernelRelease string
driverKernelVersion string
)
cmd := &cobra.Command{
Use: "driver",
DisableFlagsInUseLine: true,
Short: "Interact with falcosecurity driver",
Long: `Interact with falcosecurity driver.`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
opt.Initialize()
if err := config.Load(opt.ConfigFile); err != nil {
return err
}
// Override "version" flag with viper config if not set by user.
f := cmd.Flags().Lookup("version")
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag version")
} else if !f.Changed && viper.IsSet(config.DriverVersionKey) {
val := viper.Get(config.DriverVersionKey)
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
return fmt.Errorf("unable to overwrite \"version\" flag: %w", err)
}
}
// Override "repo" flag with viper config if not set by user.
f = cmd.Flags().Lookup("repo")
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag repo")
} else if !f.Changed && viper.IsSet(config.DriverReposKey) {
val, err := config.DriverRepos()
if err != nil {
return err
}
if err := cmd.Flags().Set(f.Name, strings.Join(val, ",")); err != nil {
return fmt.Errorf("unable to overwrite \"repo\" flag: %w", err)
}
}
// Override "name" flag with viper config if not set by user.
f = cmd.Flags().Lookup("name")
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag name")
} else if !f.Changed && viper.IsSet(config.DriverNameKey) {
val := viper.Get(config.DriverNameKey)
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
return fmt.Errorf("unable to overwrite \"name\" flag: %w", err)
}
}
// Override "host-root" flag with viper config if not set by user.
f = cmd.Flags().Lookup("host-root")
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag host-root")
} else if !f.Changed && viper.IsSet(config.DriverHostRootKey) {
val := viper.Get(config.DriverHostRootKey)
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
return fmt.Errorf("unable to overwrite \"host-root\" flag: %w", err)
}
}
// Override "type" flag with viper config if not set by user.
f = cmd.Flags().Lookup("type")
if f == nil {
// should never happen
return fmt.Errorf("unable to retrieve flag type")
} else if !f.Changed && viper.IsSet(config.DriverTypeKey) {
val, err := config.DriverTypes()
if err != nil {
return err
}
if err := cmd.Flags().Set(f.Name, strings.Join(val, ",")); err != nil {
return fmt.Errorf("unable to overwrite \"type\" flag: %w", err)
}
}
// Logic to discover correct driver to be used
// Step 1: build up allowed driver types
allowedDriverTypes := make([]drivertype.DriverType, 0)
for _, dTypeStr := range driverTypesStr {
// Ok driver type was enforced by the user
drvType, err := drivertype.Parse(dTypeStr)
if err != nil {
return err
}
allowedDriverTypes = append(allowedDriverTypes, drvType)
opt.Printer.Logger.Debug("Allowed driver",
opt.Printer.Logger.Args("type", drvType))
}
// Step 2: fetch system info (kernel release/version and distro)
var err error
driver.Kr, err = driverkernel.FetchInfo(driverKernelRelease, driverKernelVersion)
if err != nil {
return err
}
opt.Printer.Logger.Debug("Fetched kernel info", opt.Printer.Logger.Args(
"arch", driver.Kr.Architecture.ToNonDeb(),
"kernel release", driver.Kr.String(),
"kernel version", driver.Kr.KernelVersion))
driver.Distro, err = driverdistro.Discover(driver.Kr, driver.HostRoot)
if err != nil {
if !errors.Is(err, driverdistro.ErrUnsupported) {
return err
}
opt.Printer.Logger.Debug("Detected an unsupported target system; falling back at generic logic.")
}
opt.Printer.Logger.Debug("Discovered distro", opt.Printer.Logger.Args("target", driver.Distro))
driver.Type = driver.Distro.PreferredDriver(driver.Kr, allowedDriverTypes)
if driver.Type == nil {
return fmt.Errorf("no supported driver found for distro: %s, "+
"kernelrelease %s, "+
"kernelversion %s, "+
"arch %s",
driver.Distro.String(),
driver.Kr.String(),
driver.Kr.KernelVersion,
driver.Kr.Architecture.ToNonDeb())
}
opt.Printer.Logger.Debug("Detected supported driver", opt.Printer.Logger.Args("type", driver.Type.String()))
// If empty, try to load it automatically from /usr/src sub folders,
// using the most recent (ie: the one with greatest semver) driver version.
if driver.Version == "" {
driver.Version = loadDriverVersion()
}
return driver.Validate()
},
}
cmd.PersistentFlags().StringSliceVar(&driverTypesStr, "type", config.DefaultDriver.Type,
"Driver types allowed in descending priority order "+driverTypesEnum.Allowed())
cmd.PersistentFlags().StringVar(&driver.Version, "version", config.DefaultDriver.Version, "Driver version to be used.")
cmd.PersistentFlags().StringSliceVar(&driver.Repos, "repo", config.DefaultDriver.Repos, "Driver repo to be used.")
cmd.PersistentFlags().StringVar(&driver.Name, "name", config.DefaultDriver.Name, "Driver name to be used.")
cmd.PersistentFlags().StringVar(&driver.HostRoot, "host-root", config.DefaultDriver.HostRoot, "Driver host root to be used.")
cmd.PersistentFlags().StringVar(&driverKernelRelease,
"kernelrelease",
"",
"Specify the kernel release for which to download/build the driver in the same format used by 'uname -r' "+
"(e.g. '6.1.0-10-cloud-amd64')")
cmd.PersistentFlags().StringVar(&driverKernelVersion,
"kernelversion",
"",
"Specify the kernel version for which to download/build the driver in the same format used by 'uname -v' "+
"(e.g. '#1 SMP PREEMPT_DYNAMIC Debian 6.1.38-2 (2023-07-27)')")
cmd.AddCommand(driverinstall.NewDriverInstallCmd(ctx, opt, driver))
cmd.AddCommand(driverconfig.NewDriverConfigCmd(ctx, opt, driver))
cmd.AddCommand(drivercleanup.NewDriverCleanupCmd(ctx, opt, driver))
cmd.AddCommand(driverprintenv.NewDriverPrintenvCmd(ctx, opt, driver))
return cmd
}
func loadDriverVersion() string {
isSet := false
greatestVrs := semver.Version{}
paths, _ := filepath.Glob("/usr/src/falco-*")
for _, path := range paths {
fileInfo, err := os.Stat(path)
// We expect path to point to a folder,
// otherwise skip it.
if err != nil {
continue
}
if !fileInfo.IsDir() {
continue
}
drvVer := strings.TrimPrefix(filepath.Base(path), "falco-")
sv, err := semver.Parse(drvVer)
if err != nil {
// Not a semver; return it because we
// Won't be able to check it against semver driver versions.
return drvVer
}
if sv.GT(greatestVrs) {
greatestVrs = sv
isSet = true
}
}
if isSet {
return greatestVrs.String()
}
return ""
}

View File

@ -0,0 +1,30 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2024 The Falco Authors
//
// 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.
//go:build !linux
package driver
import (
"github.com/spf13/cobra"
"golang.org/x/net/context"
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
)
// NewDriverCmd returns an empty driver command since it is not supported on non linuxes
func NewDriverCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Command {
return &cobra.Command{}
}

17
cmd/driver/install/doc.go Normal file
View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 driverinstall defines the installation logic for the driver cmd.
package driverinstall

View File

@ -0,0 +1,219 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2024 The Falco Authors
//
// 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 driverinstall
import (
"bytes"
"crypto/tls"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"golang.org/x/net/context"
driverdistro "github.com/falcosecurity/falcoctl/pkg/driver/distro"
"github.com/falcosecurity/falcoctl/pkg/options"
)
type driverDownloadOptions struct {
InsecureDownload bool
HTTPTimeout time.Duration
HTTPHeaders string
}
type driverInstallOptions struct {
*options.Common
*options.Driver
Download bool
Compile bool
DownloadHeaders bool
driverDownloadOptions
}
// NewDriverInstallCmd returns the driver install command.
func NewDriverInstallCmd(ctx context.Context, opt *options.Common, driver *options.Driver) *cobra.Command {
o := driverInstallOptions{
Common: opt,
Driver: driver,
// Defaults to downloading or building if needed
Download: true,
Compile: true,
}
cmd := &cobra.Command{
Use: "install [flags]",
DisableFlagsInUseLine: true,
Short: "Install previously configured driver",
Long: `Install previously configured driver, either downloading it or attempting a build.`,
RunE: func(cmd *cobra.Command, args []string) error {
dest, err := o.RunDriverInstall(ctx)
if dest != "" {
// We don't care about errors at this stage
// Fallback: try to load any available driver if leaving with an error.
// It is only useful for kmod, as it will try to
// modprobe a pre-existent version of the driver,
// hoping it will be compatible.
_ = driver.Type.Load(o.Printer, dest, o.Driver.Name, err != nil)
}
return err
},
}
cmd.Flags().BoolVar(&o.Download, "download", true, "Whether to enable download of prebuilt drivers")
cmd.Flags().BoolVar(&o.Compile, "compile", true, "Whether to enable local compilation of drivers")
cmd.Flags().BoolVar(&o.DownloadHeaders, "download-headers", true, "Whether to enable automatic kernel headers download where supported")
cmd.Flags().BoolVar(&o.InsecureDownload, "http-insecure", false, "Whether you want to allow insecure downloads or not")
cmd.Flags().DurationVar(&o.HTTPTimeout, "http-timeout", 60*time.Second, "Timeout for each http try")
cmd.Flags().StringVar(&o.HTTPHeaders, "http-headers",
"",
"Optional comma-separated list of headers for the http GET request "+
"(e.g. --http-headers='x-emc-namespace: default,Proxy-Authenticate: Basic'). Not necessary if default repo is used")
return cmd
}
//nolint:gosec // this was an existent option in falco-driver-loader that we are porting.
func setDefaultHTTPClientOpts(downloadOptions driverDownloadOptions) {
// Skip insecure verify
if downloadOptions.InsecureDownload {
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
http.DefaultClient.Timeout = downloadOptions.HTTPTimeout
}
// RunDriverInstall implements the driver install command.
func (o *driverInstallOptions) RunDriverInstall(ctx context.Context) (string, error) {
o.Printer.Logger.Info("Running falcoctl driver install", o.Printer.Logger.Args(
"driver version", o.Driver.Version,
"driver type", o.Driver.Type,
"driver name", o.Driver.Name,
"compile", o.Compile,
"download", o.Download,
"target", o.Distro.String(),
"arch", o.Kr.Architecture.ToNonDeb(),
"kernel release", o.Kr.String(),
"kernel version", o.Kr.KernelVersion))
if !o.Driver.Type.HasArtifacts() {
o.Printer.Logger.Info("No artifacts needed for the selected driver.")
return "", nil
}
if !o.Download && !o.Compile {
o.Printer.Logger.Info("Nothing to do: download and compile disabled.")
return "", nil
}
if o.Distro.String() == driverdistro.UndeterminedDistro {
if o.Compile {
o.Download = false
o.Printer.Logger.Info(
"Detected an unsupported target system, please get in touch with the Falco community. Trying to compile anyway.")
} else {
return "", fmt.Errorf("detected an unsupported target system, please get in touch with the Falco community")
}
}
var (
dest string
buf bytes.Buffer
)
if !o.Printer.DisableStyling {
o.Printer.Spinner, _ = o.Printer.Spinner.Start("Cleaning up existing drivers")
}
err := o.Driver.Type.Cleanup(o.Printer.WithWriter(&buf), o.Driver.Name)
if o.Printer.Spinner != nil {
_ = o.Printer.Spinner.Stop()
}
if o.Printer.Logger.Formatter == pterm.LogFormatterJSON {
// Only print formatted text if we are formatting to json
out := strings.ReplaceAll(buf.String(), "\n", ";")
o.Printer.Logger.Info("Driver cleanup", o.Printer.Logger.Args("output", out))
} else {
// Print much more readable output as-is
o.Printer.DefaultText.Print(buf.String())
}
buf.Reset()
if err != nil {
return "", err
}
if o.Download {
setDefaultHTTPClientOpts(o.driverDownloadOptions)
if !o.Printer.DisableStyling {
o.Printer.Spinner, _ = o.Printer.Spinner.Start("Trying to download the driver")
}
dest, err = driverdistro.Download(ctx, o.Distro, o.Printer.WithWriter(&buf), o.Kr, o.Driver.Name,
o.Driver.Type, o.Driver.Version, o.Driver.Repos, o.HTTPHeaders)
if o.Printer.Spinner != nil {
_ = o.Printer.Spinner.Stop()
}
if o.Printer.Logger.Formatter == pterm.LogFormatterJSON {
// Only print formatted text if we are formatting to json
out := strings.ReplaceAll(buf.String(), "\n", ";")
o.Printer.Logger.Info("Driver download", o.Printer.Logger.Args("output", out))
} else {
// Print much more readable output as-is
o.Printer.DefaultText.Print(buf.String())
}
buf.Reset()
if err == nil {
o.Printer.Logger.Info("Driver downloaded.", o.Printer.Logger.Args("path", dest))
return dest, nil
}
if errors.Is(err, driverdistro.ErrAlreadyPresent) {
o.Printer.Logger.Info("Skipping download, driver already present.", o.Printer.Logger.Args("path", dest))
return dest, nil
}
// Print the error but go on
// attempting a build if requested
if o.Compile {
o.Printer.Logger.Warn(err.Error())
}
}
if o.Compile {
if !o.Printer.DisableStyling {
o.Printer.Spinner, _ = o.Printer.Spinner.Start("Trying to build the driver")
}
dest, err = driverdistro.Build(ctx, o.Distro, o.Printer.WithWriter(&buf), o.Kr, o.Driver.Name, o.Driver.Type, o.Driver.Version, o.DownloadHeaders)
if o.Printer.Spinner != nil {
_ = o.Printer.Spinner.Stop()
}
if o.Printer.Logger.Formatter == pterm.LogFormatterJSON {
// Only print formatted text if we are formatting to json
out := strings.ReplaceAll(buf.String(), "\n", ";")
o.Printer.Logger.Info("Driver build", o.Printer.Logger.Args("output", out))
} else {
// Print much more readable output as-is
o.Printer.DefaultText.Print(buf.String())
}
buf.Reset()
if err == nil {
return dest, nil
}
if errors.Is(err, driverdistro.ErrAlreadyPresent) {
o.Printer.Logger.Info("Skipping build, driver already present.", o.Printer.Logger.Args("path", dest))
return dest, nil
}
}
return o.Driver.Name, fmt.Errorf("failed: %w", err)
}

View File

@ -0,0 +1,69 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 driverinstall_test
import (
"context"
"os"
"path/filepath"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/spf13/cobra"
"github.com/falcosecurity/falcoctl/cmd"
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
testutils "github.com/falcosecurity/falcoctl/pkg/test"
)
var (
ctx = context.Background()
output = gbytes.NewBuffer()
rootCmd *cobra.Command
opt *commonoptions.Common
configFile string
err error
args []string
)
func TestInstall(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Install Suite")
}
var _ = BeforeSuite(func() {
// Create and configure the common options.
opt = commonoptions.NewOptions()
opt.Initialize(commonoptions.WithWriter(output))
// Create temporary directory used to save the configuration file.
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
Expect(err).Should(Succeed())
})
var _ = AfterSuite(func() {
configDir := filepath.Dir(configFile)
Expect(os.RemoveAll(configDir)).Should(Succeed())
})
func executeRoot(args []string) error {
rootCmd.SetArgs(args)
rootCmd.SetOut(output)
return cmd.Execute(rootCmd, opt)
}

View File

@ -0,0 +1,130 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2024 The Falco Authors
//
// 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 driverinstall_test
import (
"regexp"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/falcosecurity/falcoctl/cmd"
)
//nolint:lll // no need to check for line length.
var driverInstallHelp = `Install previously configured driver, either downloading it or attempting a build.
Usage:
falcoctl driver install [flags]
Flags:
--compile Whether to enable local compilation of drivers (default true)
--download Whether to enable download of prebuilt drivers (default true)
--download-headers Whether to enable automatic kernel headers download where supported (default true)
-h, --help help for install
--http-headers string Optional comma-separated list of headers for the http GET request (e.g. --http-headers='x-emc-namespace: default,Proxy-Authenticate: Basic'). Not necessary if default repo is used
--http-insecure Whether you want to allow insecure downloads or not
--http-timeout duration Timeout for each http try (default 1m0s)
Global Flags:
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
--host-root string Driver host root to be used. (default "/")
--kernelrelease string Specify the kernel release for which to download/build the driver in the same format used by 'uname -r' (e.g. '6.1.0-10-cloud-amd64')
--kernelversion string Specify the kernel version for which to download/build the driver in the same format used by 'uname -v' (e.g. '#1 SMP PREEMPT_DYNAMIC Debian 6.1.38-2 (2023-07-27)')
--log-format string Set formatting for logs (color, text, json) (default "color")
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
--name string Driver name to be used. (default "falco")
--repo strings Driver repo to be used. (default [https://download.falco.org/driver])
--type strings Driver types allowed in descending priority order (ebpf, kmod, modern_ebpf) (default [modern_ebpf,kmod,ebpf])
--version string Driver version to be used.
`
var addAssertFailedBehavior = func(specificError string) {
It("check that fails and the usage is not printed", func() {
Expect(err).To(HaveOccurred())
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
})
}
var addAssertOkBehavior = func(specificOut string) {
It("check that does not fail and the usage is not printed", func() {
Succeed()
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificOut)))
})
}
var _ = Describe("install", func() {
var (
driverCmd = "driver"
installCmd = "install"
)
// Each test gets its own root command and runs it.
// The err variable is asserted by each test.
JustBeforeEach(func() {
rootCmd = cmd.New(ctx, opt)
err = executeRoot(args)
})
JustAfterEach(func() {
Expect(output.Clear()).ShouldNot(HaveOccurred())
})
Context("help message", func() {
BeforeEach(func() {
args = []string{driverCmd, installCmd, "--help"}
})
It("should match the saved one", func() {
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(driverInstallHelp)))
})
})
// Here we are testing failure cases for installing a driver.
Context("failure", func() {
When("with empty driver version", func() {
BeforeEach(func() {
args = []string{driverCmd, installCmd, "--config", configFile}
})
addAssertFailedBehavior(`ERROR version is mandatory and cannot be empty`)
})
When("with non absolute host-root", func() {
BeforeEach(func() {
args = []string{driverCmd, installCmd, "--config", configFile, "--host-root", "foo/", "--version", "1.0.0+driver"}
})
addAssertFailedBehavior("ERROR host-root must be an absolute path (foo/)")
})
When("with invalid driver type", func() {
BeforeEach(func() {
args = []string{driverCmd, installCmd, "--config", configFile, "--type", "foo", "--version", "1.0.0+driver"}
})
addAssertFailedBehavior(`ERROR unsupported driver type specified: foo`)
})
})
Context("nothing-to-do", func() {
When("with false download and compile", func() {
BeforeEach(func() {
args = []string{driverCmd, installCmd, "--config", configFile, "--download=false", "--compile=false", "--version", "1.0.0+driver"}
})
addAssertOkBehavior("INFO Nothing to do: download and compile disabled.")
})
})
})

View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 driverprintenv defines the logic to print driver-related variables as env vars.
package driverprintenv

View File

@ -0,0 +1,65 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2024 The Falco Authors
//
// 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 driverprintenv
import (
"strings"
"github.com/spf13/cobra"
"golang.org/x/net/context"
"github.com/falcosecurity/falcoctl/pkg/options"
)
type driverPrintenvOptions struct {
*options.Common
*options.Driver
}
// NewDriverPrintenvCmd print info about driver falcoctl config as env vars.
func NewDriverPrintenvCmd(ctx context.Context, opt *options.Common, driver *options.Driver) *cobra.Command {
o := driverPrintenvOptions{
Common: opt,
Driver: driver,
}
cmd := &cobra.Command{
Use: "printenv [flags]",
DisableFlagsInUseLine: true,
Short: "Print env vars",
Long: `Print variables used by driver as env vars.`,
RunE: func(_ *cobra.Command, _ []string) error {
return o.RunDriverPrintenv(ctx)
},
}
return cmd
}
func (o *driverPrintenvOptions) RunDriverPrintenv(_ context.Context) error {
o.Printer.DefaultText.Printf("DRIVER=%q\n", o.Driver.Type.String())
o.Printer.DefaultText.Printf("DRIVERS_REPO=%q\n", strings.Join(o.Driver.Repos, ", "))
o.Printer.DefaultText.Printf("DRIVER_VERSION=%q\n", o.Driver.Version)
o.Printer.DefaultText.Printf("DRIVER_NAME=%q\n", o.Driver.Name)
o.Printer.DefaultText.Printf("HOST_ROOT=%q\n", o.Driver.HostRoot)
o.Printer.DefaultText.Printf("TARGET_ID=%q\n", o.Distro.String())
o.Printer.DefaultText.Printf("ARCH=%q\n", o.Kr.Architecture.ToNonDeb())
o.Printer.DefaultText.Printf("KERNEL_RELEASE=%q\n", o.Kr.String())
o.Printer.DefaultText.Printf("KERNEL_VERSION=%q\n", o.Kr.KernelVersion)
fixedKr := o.Distro.FixupKernel(o.Kr)
o.Printer.DefaultText.Printf("FIXED_KERNEL_RELEASE=%q\n", fixedKr.String())
o.Printer.DefaultText.Printf("FIXED_KERNEL_VERSION=%q\n", fixedKr.KernelVersion)
return nil
}

View File

@ -0,0 +1,69 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 driverprintenv_test
import (
"context"
"os"
"path/filepath"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/spf13/cobra"
"github.com/falcosecurity/falcoctl/cmd"
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
testutils "github.com/falcosecurity/falcoctl/pkg/test"
)
var (
ctx = context.Background()
output = gbytes.NewBuffer()
rootCmd *cobra.Command
opt *commonoptions.Common
configFile string
err error
args []string
)
func TestPrintenv(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Printenv Suite")
}
var _ = BeforeSuite(func() {
// Create and configure the common options.
opt = commonoptions.NewOptions()
opt.Initialize(commonoptions.WithWriter(output))
// Create temporary directory used to save the configuration file.
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
Expect(err).Should(Succeed())
})
var _ = AfterSuite(func() {
configDir := filepath.Dir(configFile)
Expect(os.RemoveAll(configDir)).Should(Succeed())
})
func executeRoot(args []string) error {
rootCmd.SetArgs(args)
rootCmd.SetOut(output)
return cmd.Execute(rootCmd, opt)
}

View File

@ -0,0 +1,146 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2024 The Falco Authors
//
// 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 driverprintenv_test
import (
"bufio"
"os"
"regexp"
"strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/falcosecurity/falcoctl/cmd"
)
//nolint:lll // no need to check for line length.
var driverPrintenvHelp = `Print variables used by driver as env vars.
Usage:
falcoctl driver printenv [flags]
Flags:
-h, --help help for printenv
Global Flags:
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
--host-root string Driver host root to be used. (default "/")
--kernelrelease string Specify the kernel release for which to download/build the driver in the same format used by 'uname -r' (e.g. '6.1.0-10-cloud-amd64')
--kernelversion string Specify the kernel version for which to download/build the driver in the same format used by 'uname -v' (e.g. '#1 SMP PREEMPT_DYNAMIC Debian 6.1.38-2 (2023-07-27)')
--log-format string Set formatting for logs (color, text, json) (default "color")
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
--name string Driver name to be used. (default "falco")
--repo strings Driver repo to be used. (default [https://download.falco.org/driver])
--type strings Driver types allowed in descending priority order (ebpf, kmod, modern_ebpf) (default [modern_ebpf,kmod,ebpf])
--version string Driver version to be used.
`
var driverPrintenvDefaultConfig = `DRIVER=".*"
DRIVERS_REPO="https:\/\/download\.falco\.org\/driver"
DRIVER_VERSION="1.0.0\+driver"
DRIVER_NAME="falco"
HOST_ROOT="\/"
TARGET_ID=".*"
ARCH="x86_64|aarch64"
KERNEL_RELEASE=".*"
KERNEL_VERSION=".*"
FIXED_KERNEL_RELEASE=".*"
FIXED_KERNEL_VERSION=".*"
`
var addAssertFailedBehavior = func(specificError string) {
It("check that fails and the usage is not printed", func() {
Expect(err).To(HaveOccurred())
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
})
}
var _ = Describe("printenv", func() {
var (
driverCmd = "driver"
printenvCmd = "printenv"
)
// Each test gets its own root command and runs it.
// The err variable is asserted by each test.
JustBeforeEach(func() {
rootCmd = cmd.New(ctx, opt)
err = executeRoot(args)
})
JustAfterEach(func() {
Expect(output.Clear()).ShouldNot(HaveOccurred())
})
Context("help message", func() {
BeforeEach(func() {
args = []string{driverCmd, printenvCmd, "--help"}
})
It("should match the saved one", func() {
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(driverPrintenvHelp)))
})
})
// Here we are testing failure cases for cleaning a driver.
Context("failure", func() {
When("with empty driver version", func() {
BeforeEach(func() {
args = []string{driverCmd, printenvCmd, "--config", configFile}
})
addAssertFailedBehavior(`ERROR version is mandatory and cannot be empty `)
})
When("with non absolute host-root", func() {
BeforeEach(func() {
args = []string{driverCmd, printenvCmd, "--config", configFile, "--host-root", "foo/", "--version", "1.0.0+driver"}
})
addAssertFailedBehavior("ERROR host-root must be an absolute path (foo/)")
})
When("with invalid driver type", func() {
BeforeEach(func() {
args = []string{driverCmd, printenvCmd, "--config", configFile, "--type", "foo", "--version", "1.0.0+driver"}
})
addAssertFailedBehavior(`unsupported driver type specified: foo`)
})
})
Context("success", func() {
When("with default config values", func() {
BeforeEach(func() {
args = []string{driverCmd, printenvCmd, "--config", configFile, "--version", "1.0.0+driver"}
})
It("should match the saved one", func() {
Succeed()
MatchRegexp(driverPrintenvDefaultConfig)
Expect(string(output.Contents())).To(MatchRegexp(driverPrintenvDefaultConfig))
// Expect that output is bash setenv compatible
scanner := bufio.NewScanner(output)
for scanner.Scan() {
vals := strings.Split(scanner.Text(), "=")
Expect(vals).Should(HaveLen(2))
err := os.Setenv(vals[0], vals[1])
Expect(err).Should(BeNil())
}
})
})
})
})

95
cmd/index/add/add.go Normal file
View File

@ -0,0 +1,95 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 add
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/falcosecurity/falcoctl/internal/config"
"github.com/falcosecurity/falcoctl/pkg/index/cache"
"github.com/falcosecurity/falcoctl/pkg/options"
)
// IndexAddOptions contains the options for the index add command.
type IndexAddOptions struct {
*options.Common
}
// NewIndexAddCmd returns the index add command.
func NewIndexAddCmd(ctx context.Context, opt *options.Common) *cobra.Command {
o := IndexAddOptions{
Common: opt,
}
cmd := &cobra.Command{
Use: "add [NAME] [URL] [BACKEND] [flags]",
DisableFlagsInUseLine: true,
Short: "Add an index to the local falcoctl configuration",
Long: "Add an index to the local falcoctl configuration. Indexes are used to perform search operations for artifacts",
Args: cobra.RangeArgs(2, 3),
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunIndexAdd(ctx, args)
},
}
return cmd
}
// RunIndexAdd implements the index add command.
func (o *IndexAddOptions) RunIndexAdd(ctx context.Context, args []string) error {
var err error
logger := o.Printer.Logger
name := args[0]
url := args[1]
backend := ""
if len(args) > 2 {
backend = args[2]
}
logger.Debug("Creating in-memory cache using", logger.Args("indexes file", config.IndexesFile, "indexes directory", config.IndexesDir))
indexCache, err := cache.New(ctx, config.IndexesFile, config.IndexesDir)
if err != nil {
return fmt.Errorf("unable to create index cache: %w", err)
}
logger.Info("Adding index", logger.Args("name", name, "path", url))
if err = indexCache.Add(ctx, name, backend, url); err != nil {
return fmt.Errorf("unable to add index: %w", err)
}
logger.Debug("Writing cache to disk")
if _, err = indexCache.Write(); err != nil {
return fmt.Errorf("unable to write cache to disk: %w", err)
}
logger.Debug("Adding new index entry to configuration", logger.Args("file", o.ConfigFile))
if err = config.AddIndexes([]config.Index{{
Name: name,
URL: url,
Backend: backend,
}}, o.ConfigFile); err != nil {
return fmt.Errorf("index entry %q: %w", name, err)
}
logger.Info("Index successfully added")
return nil
}

View File

@ -0,0 +1,87 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 add_test
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/spf13/cobra"
"oras.land/oras-go/v2/registry/remote"
"github.com/falcosecurity/falcoctl/cmd"
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
testutils "github.com/falcosecurity/falcoctl/pkg/test"
)
//nolint:unused // false positive
const (
rulesfiletgz = "../../../pkg/test/data/rules.tar.gz"
rulesfileyaml = "../../../pkg/test/data/rules.yaml"
plugintgz = "../../../pkg/test/data/plugin.tar.gz"
)
//nolint:unused // false positive
var (
registry string
ctx = context.Background()
output = gbytes.NewBuffer()
rootCmd *cobra.Command
opt *commonoptions.Common
port int
orasRegistry *remote.Registry
configFile string
err error
args []string
)
func TestAdd(t *testing.T) {
RegisterFailHandler(Fail)
port, err = testutils.FreePort()
Expect(err).ToNot(HaveOccurred())
registry = fmt.Sprintf("localhost:%d", port)
RunSpecs(t, "Add Suite")
}
var _ = BeforeSuite(func() {
// Create and configure the common options.
opt = commonoptions.NewOptions()
opt.Initialize(commonoptions.WithWriter(output))
// Create temporary directory used to save the configuration file.
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
Expect(err).Should(Succeed())
})
var _ = AfterSuite(func() {
configDir := filepath.Dir(configFile)
Expect(os.RemoveAll(configDir)).Should(Succeed())
})
//nolint:unused // false positive
func executeRoot(args []string) error {
rootCmd.SetArgs(args)
rootCmd.SetOut(output)
return cmd.Execute(rootCmd, opt)
}

121
cmd/index/add/add_test.go Normal file
View File

@ -0,0 +1,121 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 add_test
import (
"regexp"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/falcosecurity/falcoctl/cmd"
)
//nolint:lll // no need to check for line length.
var indexAddUsage = `Usage:
falcoctl index add [NAME] [URL] [BACKEND] [flags]
Flags:
-h, --help help for add
Global Flags:
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
--log-format string Set formatting for logs (color, text, json) (default "color")
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
`
//nolint:lll // no need to check for line length.
var indexAddHelp = `Add an index to the local falcoctl configuration. Indexes are used to perform search operations for artifacts
Usage:
falcoctl index add [NAME] [URL] [BACKEND] [flags]
Flags:
-h, --help help for add
Global Flags:
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
--log-format string Set formatting for logs (color, text, json) (default "color")
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
`
var addAssertFailedBehavior = func(usage, specificError string) {
It("check that fails and the usage is not printed", func() {
Expect(err).To(HaveOccurred())
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
})
}
var indexAddTests = Describe("add", func() {
var (
indexCmd = "index"
addCmd = "add"
indexName = "testName"
)
// Each test gets its own root command and runs it.
// The err variable is asserted by each test.
JustBeforeEach(func() {
rootCmd = cmd.New(ctx, opt)
err = executeRoot(args)
})
JustAfterEach(func() {
Expect(output.Clear()).ShouldNot(HaveOccurred())
})
Context("help message", func() {
BeforeEach(func() {
args = []string{indexCmd, addCmd, "--help"}
})
It("should match the saved one", func() {
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(indexAddHelp)))
})
})
// Here we are testing failure cases for adding a new index.
Context("failure", func() {
When("without URL", func() {
BeforeEach(func() {
args = []string{indexCmd, addCmd, "--config", configFile, indexName}
})
addAssertFailedBehavior(indexAddUsage, "ERROR accepts between 2 and 3 arg(s), received 1")
})
When("with invalid URL", func() {
BeforeEach(func() {
args = []string{indexCmd, addCmd, "--config", configFile, indexName, "NOTAPROTOCAL://something"}
})
addAssertFailedBehavior(indexAddUsage, "ERROR unable to add index: unable to fetch index \"testName\""+
" with URL \"NOTAPROTOCAL://something\": unable to fetch index: cannot fetch index: Get "+
"\"notaprotocal://something\": unsupported protocol scheme \"notaprotocal\"")
})
When("with invalid backend", func() {
BeforeEach(func() {
args = []string{indexCmd, addCmd, "--config", configFile, indexName, "http://noindex", "notabackend"}
})
addAssertFailedBehavior(indexAddUsage, "ERROR unable to add index: unable to fetch index \"testName\" "+
"with URL \"http://noindex\": unsupported index backend type: notabackend")
})
})
})

17
cmd/index/add/doc.go Normal file
View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 add defines the options and add logic for the index files.
package add

17
cmd/index/doc.go Normal file
View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 index implements the index commands.
package index

50
cmd/index/index.go Normal file
View File

@ -0,0 +1,50 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 index
import (
"context"
"github.com/spf13/cobra"
"github.com/falcosecurity/falcoctl/cmd/index/add"
"github.com/falcosecurity/falcoctl/cmd/index/list"
"github.com/falcosecurity/falcoctl/cmd/index/remove"
"github.com/falcosecurity/falcoctl/cmd/index/update"
"github.com/falcosecurity/falcoctl/internal/config"
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
)
// NewIndexCmd returns the index command.
func NewIndexCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Command {
cmd := &cobra.Command{
Use: "index",
DisableFlagsInUseLine: true,
Short: "Interact with index",
Long: "Interact with index",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
opt.Initialize()
return config.Load(opt.ConfigFile)
},
}
cmd.AddCommand(add.NewIndexAddCmd(ctx, opt))
cmd.AddCommand(remove.NewIndexRemoveCmd(ctx, opt))
cmd.AddCommand(update.NewIndexUpdateCmd(ctx, opt))
cmd.AddCommand(list.NewIndexListCmd(ctx, opt))
return cmd
}

17
cmd/index/list/doc.go Normal file
View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 list defines the logic to list the already configured index files.
package list

67
cmd/index/list/list.go Normal file
View File

@ -0,0 +1,67 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 list
import (
"context"
"github.com/spf13/cobra"
"github.com/falcosecurity/falcoctl/internal/config"
indexConf "github.com/falcosecurity/falcoctl/pkg/index/config"
"github.com/falcosecurity/falcoctl/pkg/options"
"github.com/falcosecurity/falcoctl/pkg/output"
)
type indexListOptions struct {
*options.Common
}
// NewIndexListCmd returns the index list command.
func NewIndexListCmd(_ context.Context, opt *options.Common) *cobra.Command {
o := indexListOptions{
Common: opt,
}
cmd := &cobra.Command{
Use: "list [flags]",
DisableFlagsInUseLine: true,
Short: "List all the added indexes",
Long: "List all the added indexes that were configured in falcoctl",
Args: cobra.ExactArgs(0),
Aliases: []string{"ls"},
RunE: func(cmd *cobra.Command, _ []string) error {
return o.RunIndexList()
},
}
return cmd
}
func (o *indexListOptions) RunIndexList() error {
indexConfig, err := indexConf.New(config.IndexesFile)
if err != nil {
return err
}
var data [][]string
for _, conf := range indexConfig.Configs {
newEntry := []string{conf.Name, conf.URL, conf.AddedTimestamp, conf.UpdatedTimestamp}
data = append(data, newEntry)
}
return o.Printer.PrintTable(output.IndexList, data)
}

17
cmd/index/remove/doc.go Normal file
View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 remove defines options and logic to remove a previously add index file.
package remove

View File

@ -0,0 +1,83 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 remove
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/falcosecurity/falcoctl/internal/config"
"github.com/falcosecurity/falcoctl/pkg/index/cache"
"github.com/falcosecurity/falcoctl/pkg/options"
)
type indexRemoveOptions struct {
*options.Common
}
// NewIndexRemoveCmd returns the index remove command.
func NewIndexRemoveCmd(ctx context.Context, opt *options.Common) *cobra.Command {
o := indexRemoveOptions{
Common: opt,
}
cmd := &cobra.Command{
Use: "remove [INDEX1 [INDEX2 ...]] [flags]",
DisableFlagsInUseLine: true,
Short: "Remove an index from the local falcoctl configuration",
Long: "Remove an index from the local falcoctl configuration",
Args: cobra.MinimumNArgs(1),
Aliases: []string{"rm"},
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunIndexRemove(ctx, args)
},
}
return cmd
}
func (o *indexRemoveOptions) RunIndexRemove(ctx context.Context, args []string) error {
logger := o.Printer.Logger
logger.Debug("Creating in-memory cache using", logger.Args("indexes file", config.IndexesFile, "indexes directory", config.IndexesDir))
indexCache, err := cache.New(ctx, config.IndexesFile, config.IndexesDir)
if err != nil {
return fmt.Errorf("unable to create index cache: %w", err)
}
for _, name := range args {
logger.Info("Removing index", logger.Args("name", name))
if err = indexCache.Remove(name); err != nil {
return fmt.Errorf("unable to remove index: %w", err)
}
}
logger.Debug("Writing cache to disk")
if _, err = indexCache.Write(); err != nil {
return fmt.Errorf("unable to write cache to disk: %w", err)
}
logger.Debug("Removing indexes entries from configuration", logger.Args("file", o.ConfigFile))
if err = config.RemoveIndexes(args, o.ConfigFile); err != nil {
return err
}
logger.Info("Indexes successfully removed")
return nil
}

17
cmd/index/update/doc.go Normal file
View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 update defines options and logic to update the index files.
package update

View File

@ -0,0 +1,77 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 update
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/falcosecurity/falcoctl/internal/config"
"github.com/falcosecurity/falcoctl/pkg/index/cache"
"github.com/falcosecurity/falcoctl/pkg/options"
)
type indexUpdateOptions struct {
*options.Common
}
// NewIndexUpdateCmd returns the index update command.
func NewIndexUpdateCmd(ctx context.Context, opt *options.Common) *cobra.Command {
o := indexUpdateOptions{
Common: opt,
}
cmd := &cobra.Command{
Use: "update [INDEX1 [INDEX2 ...]] [flags]",
DisableFlagsInUseLine: true,
Short: "Update an existing index",
Long: "Update an existing index",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunIndexUpdate(ctx, args)
},
}
return cmd
}
func (o *indexUpdateOptions) RunIndexUpdate(ctx context.Context, args []string) error {
logger := o.Printer.Logger
logger.Debug("Creating in-memory cache using", logger.Args("indexes file", config.IndexesFile, "indexes directory", config.IndexesDir))
indexCache, err := cache.New(ctx, config.IndexesFile, config.IndexesDir)
if err != nil {
return fmt.Errorf("unable to create index cache: %w", err)
}
for _, arg := range args {
logger.Info("Updating index file", logger.Args("name", arg))
if err := indexCache.Update(ctx, arg); err != nil {
return fmt.Errorf("an error occurred while updating index %q: %w", arg, err)
}
}
logger.Debug("Writing cache to disk")
if _, err = indexCache.Write(); err != nil {
return fmt.Errorf("unable to write cache to disk: %w", err)
}
logger.Info("Indexes successfully updated")
return nil
}

View File

@ -1,38 +0,0 @@
/*
Copyright © 2019 Kris Nova <kris@nivenly.com>
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
import (
"github.com/spf13/cobra"
)
// installCmd represents the install command
var installCmd = &cobra.Command{
Use: "install",
Short: "Install a component wih falcoctl",
Long: ``,
//Run: func(cmd *cobra.Command, args []string) {
// fmt.Println("install called")
//},
}
func init() {
rootCmd.AddCommand(installCmd)
installCmd.AddCommand(installFalcoCmd)
//installCmd.AddCommand(installOutputCmd)
//installCmd.AddCommand(installRuleCmd)
}

View File

@ -1,63 +0,0 @@
/*
Copyright © 2019 Kris Nova <kris@nivenly.com>
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
import (
"fmt"
"os"
kubernetesfalc "github.com/kris-nova/falcoctl/kubernetes"
"github.com/kubicorn/kubicorn/pkg/cli"
"github.com/kubicorn/kubicorn/pkg/logger"
"github.com/spf13/cobra"
)
var (
installFalcoCmd = &cobra.Command{
Use: "falco",
Short: "Install Falco in Kubernetes",
Long: `Deploy Falco to Kubernetes`,
Run: func(cmd *cobra.Command, args []string) {
err, exitCode := InstallFalcoEntry(i, kubeConfigPath)
if err != nil {
logger.Critical("Fatal error: %v", err)
os.Exit(exitCode)
}
logger.Always("Success.")
/// os.Exit(1)
},
}
)
// InstallFalcoEntry is used as the main entry point that someone who wants to test or vendor the code should use.
// This is the same starting place the CLI tool uses.
func InstallFalcoEntry(installer *kubernetesfalc.FalcoInstaller, kubeConfigPath string) (error, int) {
k8s, err := kubernetesfalc.NewK8sFromKubeConfigPath(kubeConfigPath)
if err != nil {
return fmt.Errorf("unable to parse kube config: %v", err), 98
}
err = installer.Install(k8s)
if err != nil {
return fmt.Errorf("unable to install falco in Kubernetes: %v", err), 99
}
return nil, 0
}
func init() {
installFalcoCmd.Flags().StringVarP(&i.DameonSetName, "ds-name", "N",
cli.StrEnvDef("FALCOCTL_KUBE_DS_NAME", "falco"), "Set the name to use with the Falco DS")
}

43
cmd/registry/auth/auth.go Normal file
View File

@ -0,0 +1,43 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 auth
import (
"context"
"github.com/spf13/cobra"
"github.com/falcosecurity/falcoctl/cmd/registry/auth/basic"
"github.com/falcosecurity/falcoctl/cmd/registry/auth/gcp"
"github.com/falcosecurity/falcoctl/cmd/registry/auth/oauth"
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
)
// NewAuthCmd returns the registry command.
func NewAuthCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
DisableFlagsInUseLine: true,
Short: "Handle authentication towards OCI registries",
Long: "Handle authentication towards OCI registries",
}
cmd.AddCommand(basic.NewBasicCmd(ctx, opt))
cmd.AddCommand(oauth.NewOauthCmd(ctx, opt))
cmd.AddCommand(gcp.NewGcpCmd(ctx, opt))
return cmd
}

View File

@ -0,0 +1,162 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 basic
import (
"bufio"
"context"
"fmt"
"io"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/term"
"oras.land/oras-go/v2/registry/remote/credentials"
"github.com/falcosecurity/falcoctl/internal/config"
"github.com/falcosecurity/falcoctl/internal/login/basic"
"github.com/falcosecurity/falcoctl/internal/utils"
"github.com/falcosecurity/falcoctl/pkg/oci/authn"
"github.com/falcosecurity/falcoctl/pkg/options"
"github.com/falcosecurity/falcoctl/pkg/output"
)
type loginOptions struct {
*options.Common
username string
password string
passwordFromStdin bool
}
// NewBasicCmd returns the basic command.
func NewBasicCmd(ctx context.Context, opt *options.Common) *cobra.Command {
o := loginOptions{
Common: opt,
}
cmd := &cobra.Command{
Use: "basic [hostname]",
DisableFlagsInUseLine: true,
Short: "Login to an OCI registry",
Long: `Login to an OCI registry
Example - Log in with username and password from command line flags:
falcoctl registry auth basic -u username -p password localhost:5000
Example - Login with username and password from env variables:
FALCOCTL_REGISTRY_AUTH_BASIC_USERNAME=username FALCOCTL_REGISTRY_AUTH_BASIC_PASSWORD=password falcoctl registry auth basic localhost:5000
Example - Login with username and password from stdin:
falcoctl registry auth basic -u username --password-stdin localhost:5000
Example - Login with username and password in an interactive prompt:
falcoctl registry auth basic localhost:5000
`,
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
_ = viper.BindPFlag("registry.auth.basic.username", cmd.Flags().Lookup("username"))
_ = viper.BindPFlag("registry.auth.basic.password", cmd.Flags().Lookup("password"))
_ = viper.BindPFlag("registry.auth.basic.password_stdin", cmd.Flags().Lookup("password-stdin"))
o.username = viper.GetString("registry.auth.basic.username")
o.password = viper.GetString("registry.auth.basic.password")
o.passwordFromStdin = viper.GetBool("registry.auth.basic.password_stdin")
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunBasic(ctx, args)
},
}
cmd.Flags().StringVarP(&o.username, "username", "u", "", "registry username")
cmd.Flags().StringVarP(&o.password, "password", "p", "", "registry password")
cmd.Flags().BoolVar(&o.passwordFromStdin, "password-stdin", false, "read password from stdin")
return cmd
}
// RunBasic executes the business logic for the basic command.
func (o *loginOptions) RunBasic(ctx context.Context, args []string) error {
var reg string
logger := o.Printer.Logger
// Allow to have the registry expressed as a ref, but actually extract it.
reg, err := utils.GetRegistryFromRef(args[0])
if err != nil {
reg = args[0]
}
if err := getCredentials(o.Printer, o); err != nil {
return err
}
// create empty client
client := authn.NewClient()
// create credential store
credentialStore, err := credentials.NewStore(config.RegistryCredentialConfPath(), credentials.StoreOptions{
AllowPlaintextPut: true,
})
if err != nil {
return fmt.Errorf("unable to create new store: %w", err)
}
if err := basic.Login(ctx, client, credentialStore, reg, o.username, o.password); err != nil {
return err
}
logger.Debug("Credentials added", logger.Args("credential store", config.RegistryCredentialConfPath()))
logger.Info("Login succeeded", logger.Args("registry", reg, "user", o.username))
return nil
}
// getCredentials is used to retrieve username and password from standard input.
func getCredentials(p *output.Printer, opt *loginOptions) error {
reader := bufio.NewReader(os.Stdin)
if opt.username == "" {
p.DefaultText.Print(p.FormatTitleAsLoggerInfo("Enter username:"))
username, err := reader.ReadString('\n')
if err != nil {
return err
}
opt.username = strings.TrimSpace(username)
}
if opt.password == "" {
if opt.passwordFromStdin {
password, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}
opt.password = strings.TrimSuffix(string(password), "\n")
opt.password = strings.TrimSuffix(opt.password, "\r")
} else {
p.DefaultText.Print(p.FormatTitleAsLoggerInfo("Enter password: "))
bytePassword, err := term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return err
}
opt.password = string(bytePassword)
}
}
return nil
}

View File

@ -0,0 +1,150 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 basic_test
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"os"
"path/filepath"
"testing"
"time"
"github.com/distribution/distribution/v3/configuration"
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/spf13/cobra"
"golang.org/x/crypto/bcrypt"
"github.com/falcosecurity/falcoctl/cmd"
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
testutils "github.com/falcosecurity/falcoctl/pkg/test"
)
//nolint:unused // false positive
var (
registry string
registryBasic string
ctx = context.Background()
output = gbytes.NewBuffer()
rootCmd *cobra.Command
opt *commonoptions.Common
port int
portBasic int
configFile string
err error
args []string
)
func TestBasic(t *testing.T) {
var err error
RegisterFailHandler(Fail)
port, err = testutils.FreePort()
Expect(err).ToNot(HaveOccurred())
portBasic, err = testutils.FreePort()
Expect(err).ToNot(HaveOccurred())
registry = fmt.Sprintf("localhost:%d", port)
registryBasic = fmt.Sprintf("localhost:%d", portBasic)
RunSpecs(t, "Auth Basic Suite")
}
var _ = BeforeSuite(func() {
config := &configuration.Configuration{}
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
testHtpasswdFileBasename := "authtest.htpasswd"
testUsername, testPassword := "username", "password"
pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost)
Expect(err).To(BeNil())
htpasswdPath := filepath.Join(GinkgoT().TempDir(), testHtpasswdFileBasename)
err = os.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0o644)
Expect(err).To(BeNil())
tlsConfig, err := testutils.BuildRegistryTLSConfig(GinkgoT().TempDir(), []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"})
Expect(err).To(BeNil())
configBasic := &configuration.Configuration{}
configBasic.HTTP.Addr = fmt.Sprintf("localhost:%d", portBasic)
configBasic.Auth = configuration.Auth{
"htpasswd": configuration.Parameters{
"realm": "localhost",
"path": htpasswdPath,
},
}
configBasic.HTTP.DrainTimeout = time.Duration(10) * time.Second
configBasic.HTTP.TLS.CipherSuites = tlsConfig.CipherSuites
configBasic.HTTP.TLS.Certificate = tlsConfig.CertificatePath
configBasic.HTTP.TLS.Key = tlsConfig.PrivateKeyPath
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
// Create and configure the common options.
opt = commonoptions.NewOptions()
opt.Initialize(commonoptions.WithWriter(output))
// Start the local registry.
go func() {
err := testutils.StartRegistry(context.Background(), config)
Expect(err).ToNot(BeNil())
}()
// Check that the registry is up and accepting connections.
Eventually(func(g Gomega) error {
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
g.Expect(err).ShouldNot(HaveOccurred())
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
return err
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
// Start the local registry with basic authentication.
go func() {
err := testutils.StartRegistry(context.Background(), configBasic)
Expect(err).ToNot(BeNil())
}()
// Check that the registry is up and accepting connections.
Eventually(func(g Gomega) error {
res, err := http.Get(fmt.Sprintf("https://%s", configBasic.HTTP.Addr))
g.Expect(err).ShouldNot(HaveOccurred())
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
return err
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
// Create temporary directory used to save the configuration file.
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
Expect(err).Should(Succeed())
})
var _ = AfterSuite(func() {
configDir := filepath.Dir(configFile)
Expect(os.RemoveAll(configDir)).Should(Succeed())
})
//nolint:unused // false positive
func executeRoot(args []string) error {
rootCmd.SetArgs(args)
rootCmd.SetOut(output)
return cmd.Execute(rootCmd, opt)
}

View File

@ -0,0 +1,141 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 basic_test
import (
"regexp"
_ "github.com/distribution/distribution/v3/registry/auth/htpasswd"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/falcosecurity/falcoctl/cmd"
)
type Config struct {
Registry Registry `yaml:"registry"`
}
type Registry struct {
Auth Auth `yaml:"auth"`
}
type Auth struct {
OAuth []OAuth `yaml:"oauth"`
}
type OAuth struct {
Registry string `yaml:"registry"`
ClientSecret string `yaml:"clientsecret"`
ClientID string `yaml:"clientid"`
TokerURL string `yaml:"tokenurl"`
}
//nolint:lll,unused // no need to check for line length.
var registryAuthBasicUsage = `Usage:
falcoctl registry auth basic [hostname]
Flags:
-h, --help help for basic
Global Flags:
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
--disable-styling Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false)
-v, --verbose Enable verbose logs (default false)
`
//nolint:unused // false positive
var registryAuthBasicHelp = `Login to an OCI registry
Example - Log in with username and password from command line flags:
falcoctl registry auth basic -u username -p password localhost:5000
Example - Login with username and password from env variables:
FALCOCTL_REGISTRY_AUTH_BASIC_USERNAME=username FALCOCTL_REGISTRY_AUTH_BASIC_PASSWORD=password falcoctl registry auth basic localhost:5000
Example - Login with username and password from stdin:
falcoctl registry auth basic -u username --password-stdin localhost:5000
Example - Login with username and password in an interactive prompt:
falcoctl registry auth basic localhost:5000
Usage:
falcoctl registry auth basic [hostname]
Flags:
-h, --help help for basic
-p, --password string registry password
--password-stdin read password from stdin
-u, --username string registry username
Global Flags:
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
--log-format string Set formatting for logs (color, text, json) (default "color")
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
`
//nolint:unused // false positive
var registryAuthBasicAssertFailedBehavior = func(usage, specificError string) {
It("check that fails and the usage is not printed", func() {
Expect(err).To(HaveOccurred())
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
})
}
//nolint:unused // false positive
var registryAuthBasicTests = Describe("auth", func() {
const (
// Used as flags for all the test cases.
registryCmd = "registry"
authCmd = "auth"
basicCmd = "basic"
)
// Each test gets its own root command and runs it.
// The err variable is asserted by each test.
JustBeforeEach(func() {
rootCmd = cmd.New(ctx, opt)
err = executeRoot(args)
})
JustAfterEach(func() {
Expect(output.Clear()).ShouldNot(HaveOccurred())
})
Context("help message", func() {
BeforeEach(func() {
args = []string{registryCmd, authCmd, basicCmd, "--help"}
})
It("should match the saved one", func() {
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(registryAuthBasicHelp)))
})
})
Context("failure", func() {
When("without hostname", func() {
BeforeEach(func() {
args = []string{registryCmd, authCmd, basicCmd}
})
registryAuthBasicAssertFailedBehavior(registryAuthBasicUsage,
"ERROR accepts 1 arg(s), received 0")
})
})
})

View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 basic defines the logic to authenticate against an OCI registry.
package basic

17
cmd/registry/auth/doc.go Normal file
View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 auth defines the logic to authenticate against an OCI registry.
package auth

View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 gcp defines the logic to authenticate against an Artifact registry using GCP credentials.
package gcp

View File

@ -0,0 +1,84 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 gcp
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/falcosecurity/falcoctl/internal/config"
"github.com/falcosecurity/falcoctl/internal/login/gcp"
"github.com/falcosecurity/falcoctl/pkg/options"
)
const (
longGcp = `Register an Artifact Registry to use GCP Application Default credentials to connect to it.
In particular, it can use Workload Identity or GCE metadata server to authenticate.
Example
falcoctl registry auth gcp europe-docker.pkg.dev
`
)
// RegistryGcpOptions contains the options for the registry gcp command.
type RegistryGcpOptions struct {
*options.Common
}
// NewGcpCmd returns the gcp command.
func NewGcpCmd(ctx context.Context, opt *options.Common) *cobra.Command {
o := RegistryGcpOptions{
Common: opt,
}
cmd := &cobra.Command{
Use: "gcp [REGISTRY]",
DisableFlagsInUseLine: true,
Short: "Register an Artifact Registry to log in using GCP Application Default credentials",
Long: longGcp,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunGcp(ctx, args)
},
}
return cmd
}
// RunGcp executes the business logic for the gcp command.
func (o *RegistryGcpOptions) RunGcp(ctx context.Context, args []string) error {
var err error
logger := o.Printer.Logger
reg := args[0]
if err = gcp.Login(ctx, reg); err != nil {
return err
}
logger.Info("GCP authentication successful", logger.Args("registry", reg))
logger.Debug("Adding new gcp entry to configuration", logger.Args("file", o.ConfigFile))
if err = config.AddGcp([]config.GcpAuth{{
Registry: reg,
}}, o.ConfigFile); err != nil {
return fmt.Errorf("index entry %q: %w", reg, err)
}
logger.Info("GCG authentication entry successfully added", logger.Args("registry", reg, "confgi file", o.ConfigFile))
return nil
}

View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 oauth defines the logic to authenticate against an OCI registry via OAuth2.0.
package oauth

View File

@ -0,0 +1,94 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 oauth
import (
"context"
"fmt"
"github.com/spf13/cobra"
"golang.org/x/oauth2/clientcredentials"
"github.com/falcosecurity/falcoctl/internal/config"
"github.com/falcosecurity/falcoctl/internal/login/oauth"
"github.com/falcosecurity/falcoctl/pkg/options"
"github.com/falcosecurity/falcoctl/pkg/output"
)
const (
longOauth = `Store client credentials for later OAuth2.0 authentication
Client credentials will be saved in the ~/.config directory.
Example
falcoctl registry oauth \
--token-url="http://localhost:9096/token" \
--client-id=000000 \
--client-secret=999999 --scopes="my-scope" \
hostname
`
)
// RegistryOauthOptions contains the options for the registry oauth command.
type RegistryOauthOptions struct {
*options.Common
Conf clientcredentials.Config
}
// NewOauthCmd returns the oauth command.
func NewOauthCmd(ctx context.Context, opt *options.Common) *cobra.Command {
o := RegistryOauthOptions{
Common: opt,
}
cmd := &cobra.Command{
Use: "oauth [HOSTNAME]",
DisableFlagsInUseLine: true,
Short: "Retrieve access and refresh tokens for OAuth2.0 client credentials flow authentication",
Long: longOauth,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunOAuth(ctx, args)
},
}
cmd.Flags().StringVar(&o.Conf.TokenURL, "token-url", "", "token URL used to get access and refresh tokens")
if err := cmd.MarkFlagRequired("token-url"); err != nil {
output.ExitOnErr(o.Printer, fmt.Errorf("unable to mark flag \"token-url\" as required"))
}
cmd.Flags().StringVar(&o.Conf.ClientID, "client-id", "", "client ID of the OAuth2.0 app")
if err := cmd.MarkFlagRequired("client-id"); err != nil {
output.ExitOnErr(o.Printer, fmt.Errorf("unable to mark flag \"client-id\" as required"))
}
cmd.Flags().StringVar(&o.Conf.ClientSecret, "client-secret", "", "client secret of the OAuth2.0 app")
if err := cmd.MarkFlagRequired("client-secret"); err != nil {
output.ExitOnErr(o.Printer, fmt.Errorf("unable to mark flag \"client-secret\" as required"))
return nil
}
cmd.Flags().StringSliceVar(&o.Conf.Scopes, "scopes", nil, "comma separeted list of scopes for which requesting access")
return cmd
}
// RunOAuth executes the business logic for the oauth command.
func (o *RegistryOauthOptions) RunOAuth(ctx context.Context, args []string) error {
reg := args[0]
if err := oauth.Login(ctx, reg, &o.Conf); err != nil {
return err
}
o.Printer.Logger.Info("Client credentials correctly saved", o.Printer.Logger.Args("file", config.ClientCredentialsFile))
return nil
}

View File

@ -0,0 +1,128 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 oauth_test
import (
"context"
"fmt"
"net/http"
"os"
"os/user"
"path/filepath"
"testing"
"time"
"github.com/distribution/distribution/v3/configuration"
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/spf13/cobra"
"oras.land/oras-go/v2/registry/remote"
"github.com/falcosecurity/falcoctl/cmd"
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
testutils "github.com/falcosecurity/falcoctl/pkg/test"
)
//nolint:unused // false positive
var (
registry string
oauthServer string
oauthPort int
ctx = context.Background()
output = gbytes.NewBuffer()
rootCmd *cobra.Command
opt *commonoptions.Common
port int
orasRegistry *remote.Registry
configFile string
err error
args []string
)
func TestOAuth(t *testing.T) {
var err error
RegisterFailHandler(Fail)
port, err = testutils.FreePort()
Expect(err).ToNot(HaveOccurred())
oauthPort, err = testutils.FreePort()
Expect(err).ToNot(HaveOccurred())
registry = fmt.Sprintf("localhost:%d", port)
RunSpecs(t, "OAuth Suite")
}
var _ = BeforeSuite(func() {
// Get the current user's home directory
usr, err := user.Current()
Expect(err).ToNot(HaveOccurred())
// Construct the path for the .config directory
configDir := filepath.Join(usr.HomeDir, ".config", "falcoctl")
// Check if the directory already exists
if _, err := os.Stat(configDir); os.IsNotExist(err) {
// Directory doesn't exist, create it
err := os.MkdirAll(configDir, 0o755)
Expect(err).ToNot(HaveOccurred())
}
config := &configuration.Configuration{}
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
// Create and configure the common options.
opt = commonoptions.NewOptions()
opt.Initialize(commonoptions.WithWriter(output))
// Create the oras registry.
orasRegistry, err = testutils.NewOrasRegistry(registry, true)
Expect(err).ToNot(HaveOccurred())
// Start the local registry.
go func() {
err := testutils.StartRegistry(context.Background(), config)
Expect(err).ToNot(BeNil())
}()
// Check that the registry is up and accepting connections.
Eventually(func(g Gomega) error {
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
g.Expect(err).ShouldNot(HaveOccurred())
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
return err
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
go func() {
err := testutils.StartOAuthServer(context.Background(), oauthPort)
Expect(err).ToNot(BeNil())
}()
// Create temporary directory used to save the configuration file.
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
Expect(err).Should(Succeed())
})
var _ = AfterSuite(func() {
configDir := filepath.Dir(configFile)
Expect(os.RemoveAll(configDir)).Should(Succeed())
})
//nolint:unused // false positive
func executeRoot(args []string) error {
rootCmd.SetArgs(args)
rootCmd.SetOut(output)
return cmd.Execute(rootCmd, opt)
}

View File

@ -0,0 +1,208 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 oauth_test
import (
"fmt"
"os"
"regexp"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/falcosecurity/falcoctl/cmd"
)
type Config struct {
Registry Registry `yaml:"registry"`
}
type Registry struct {
Auth Auth `yaml:"auth"`
}
type Auth struct {
OAuth []OAuth `yaml:"oauth"`
}
type OAuth struct {
Registry string `yaml:"registry"`
ClientSecret string `yaml:"clientsecret"`
ClientID string `yaml:"clientid"`
TokerURL string `yaml:"tokenurl"`
}
//nolint:unused // false positive
var correctIndexConfig = `indexes:
- name: falcosecurity
url: https://falcosecurity.github.io/falcoctl/index.yaml
`
//nolint:lll,unused // no need to check for line length.
var registryAuthOAuthUsage = `Usage:
falcoctl registry auth oauth [HOSTNAME]
Flags:
--client-id string client ID of the OAuth2.0 app
--client-secret string client secret of the OAuth2.0 app
-h, --help help for oauth
--scopes strings comma separeted list of scopes for which requesting access
--token-url string token URL used to get access and refresh tokens
Global Flags:
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
--log-format string Set formatting for logs (color, text, json) (default "color")
--log-level string Set level for logs (info, warn, debug, trace) (default "info")
`
//nolint:unused // false positive
var registryAuthOAuthHelp = `Store client credentials for later OAuth2.0 authentication
Client credentials will be saved in the ~/.config directory.
Example
falcoctl registry oauth \
--token-url="http://localhost:9096/token" \
--client-id=000000 \
--client-secret=999999 --scopes="my-scope" \
hostname
`
//nolint:unused // false positive
var registryAuthOAuthAssertFailedBehavior = func(usage, specificError string) {
It("check that fails and the usage is not printed", func() {
Expect(err).To(HaveOccurred())
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
})
}
//nolint:unused // false positive
var registryAuthOAuthTests = Describe("auth", func() {
const (
// Used as flags for all the test cases.
registryCmd = "registry"
authCmd = "auth"
oauthCmd = "oauth"
anSource = "myrepo.com/rules.git"
artifact = "generic-repo"
repo = "/" + artifact
tag = "tag"
repoAndTag = repo + ":" + tag
)
// Each test gets its own root command and runs it.
// The err variable is asserted by each test.
JustBeforeEach(func() {
rootCmd = cmd.New(ctx, opt)
err = executeRoot(args)
})
JustAfterEach(func() {
Expect(output.Clear()).ShouldNot(HaveOccurred())
})
Context("help message", func() {
BeforeEach(func() {
args = []string{registryCmd, authCmd, oauthCmd, "--help"}
})
It("should match the saved one", func() {
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(registryAuthOAuthHelp)))
})
})
Context("failure", func() {
When("without hostname", func() {
BeforeEach(func() {
args = []string{registryCmd, authCmd, oauthCmd}
})
registryAuthOAuthAssertFailedBehavior(registryAuthOAuthUsage,
"ERROR accepts 1 arg(s), received 0")
})
When("wrong client id", func() {
BeforeEach(func() {
baseDir := GinkgoT().TempDir()
configFilePath := baseDir + "/config.yaml"
content := []byte(correctIndexConfig)
err = os.WriteFile(configFilePath, content, 0o644)
Expect(err).To(BeNil())
args = []string{registryCmd, authCmd, oauthCmd,
"--client-id=000001", "--client-secret=999999",
"--token-url", fmt.Sprintf("http://localhost:%d/token", oauthPort),
"--config", configFilePath,
"127.0.0.1:5000",
}
})
registryAuthOAuthAssertFailedBehavior(registryAuthOAuthUsage,
`ERROR wrong client credentials, unable to retrieve token`)
})
When("wrong client secret", func() {
BeforeEach(func() {
// start the OAuthServer
baseDir := GinkgoT().TempDir()
configFilePath := baseDir + "/config.yaml"
content := []byte(correctIndexConfig)
err := os.WriteFile(configFilePath, content, 0o644)
Expect(err).To(BeNil())
args = []string{registryCmd, authCmd, oauthCmd,
"--client-id=000000", "--client-secret=999998",
"--token-url", fmt.Sprintf("http://localhost:%d/token", oauthPort),
"--config", configFilePath,
"127.0.0.1:5000",
}
})
registryAuthOAuthAssertFailedBehavior(registryAuthOAuthUsage,
`ERROR wrong client credentials, unable to retrieve token`)
})
})
Context("success", func() {
var (
configFilePath string
)
When("all good", func() {
BeforeEach(func() {
baseDir := GinkgoT().TempDir()
configFilePath = baseDir + "/config.yaml"
content := []byte(correctIndexConfig)
err = os.WriteFile(configFilePath, content, 0o644)
Expect(err).To(BeNil())
args = []string{registryCmd, authCmd, oauthCmd,
"--client-id=000000", "--client-secret=999999",
"--token-url", fmt.Sprintf("http://localhost:%d/token", oauthPort),
"--config", configFilePath,
registry,
}
})
It("should successed", func() {
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
`INFO Client credentials correctly saved`)))
})
})
})
})

17
cmd/registry/doc.go Normal file
View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 registry implements the registry commands.
package registry

17
cmd/registry/pull/doc.go Normal file
View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 pull defnines the logic to pull artifacts from remote repositories.
package pull

144
cmd/registry/pull/pull.go Normal file
View File

@ -0,0 +1,144 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 pull
import (
"context"
"fmt"
"runtime"
"github.com/spf13/cobra"
"github.com/falcosecurity/falcoctl/internal/utils"
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
"github.com/falcosecurity/falcoctl/pkg/options"
"github.com/falcosecurity/falcoctl/pkg/output"
)
const (
longPull = `Pull Falco "rulesfile" or "plugin" OCI artifacts from remote registry.
Artifact references are passed as arguments.
A reference is a fully qualified reference ("<registry>/<repository>"),
optionally followed by ":<tag>" (":latest" is assumed by default when no tag is given).
Example - Pull artifact "myplugin" for the platform where falcoctl is running (default) in the current working directory (default):
falcoctl registry pull localhost:5000/myplugin:latest
Example - Pull artifact "myplugin" for platform "linux/arm64" in the current working directory (default):
falcoctl registry pull localhost:5000/myplugin:latest --platform linux/arm64
Example - Pull artifact "myplugin" for platform "linux/arm64" in "myDir" directory:
falcoctl registry pull localhost:5000/myplugin:latest --platform linux/arm64 --dest-dir=./myDir
Example - Pull artifact "myrulesfile":
falcoctl registry pull localhost:5000/myrulesfile:latest
`
)
type pullOptions struct {
*options.Common
*options.Artifact
*options.Registry
destDir string
}
func (o *pullOptions) Validate() error {
return o.Artifact.Validate()
}
// NewPullCmd returns the pull command.
func NewPullCmd(ctx context.Context, opt *options.Common) *cobra.Command {
o := pullOptions{
Common: opt,
Artifact: &options.Artifact{},
Registry: &options.Registry{},
}
cmd := &cobra.Command{
Use: "pull hostname/repo[:tag|@digest] [flags]",
DisableFlagsInUseLine: true,
Short: "Pull a Falco OCI artifact from remote registry",
Long: longPull,
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
if err := o.Validate(); err != nil {
return err
}
ref := args[0]
_, err := utils.GetRegistryFromRef(ref)
if err != nil {
return err
}
o.Common.Initialize()
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.RunPull(ctx, args)
},
}
o.Registry.AddFlags(cmd)
output.ExitOnErr(o.Printer, o.Artifact.AddFlags(cmd))
cmd.Flags().StringVarP(&o.destDir, "dest-dir", "o", "", "destination dir where to save the artifacts(default: current directory)")
return cmd
}
// RunPull executes the business logic for the pull command.
func (o *pullOptions) RunPull(ctx context.Context, args []string) error {
logger := o.Printer.Logger
ref := args[0]
registry, err := utils.GetRegistryFromRef(ref)
if err != nil {
return err
}
puller, err := ociutils.Puller(o.PlainHTTP, o.Printer)
if err != nil {
return fmt.Errorf("an error occurred while creating the puller for registry %s: %w", registry, err)
}
err = ociutils.CheckConnectionForRegistry(ctx, puller.Client, o.PlainHTTP, registry)
if err != nil {
return err
}
logger.Info("Preparing to pull artifact", logger.Args("name", args[0]))
if o.destDir == "" {
logger.Info("Pulling artifact in the current directory")
} else {
logger.Info("Pulling artifact in", logger.Args("directory", o.destDir))
}
os, arch := runtime.GOOS, runtime.GOARCH
if len(o.Artifact.Platforms) > 0 {
os, arch = o.OSArch(0)
}
res, err := puller.Pull(ctx, ref, o.destDir, os, arch)
if err != nil {
return err
}
logger.Info("Artifact pulled", logger.Args("name", args[0], "type", res.Type, "digest", res.Digest))
return nil
}

View File

@ -0,0 +1,110 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 pull_test
import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"testing"
"time"
"github.com/distribution/distribution/v3/configuration"
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"github.com/spf13/cobra"
"oras.land/oras-go/v2/registry/remote"
"github.com/falcosecurity/falcoctl/cmd"
commonoptions "github.com/falcosecurity/falcoctl/pkg/options"
testutils "github.com/falcosecurity/falcoctl/pkg/test"
)
//nolint:unused // false positive
const (
rulesfiletgz = "../../../pkg/test/data/rules.tar.gz"
rulesfileyaml = "../../../pkg/test/data/rules.yaml"
plugintgz = "../../../pkg/test/data/plugin.tar.gz"
)
//nolint:unused // false positive
var (
registry string
ctx = context.Background()
output = gbytes.NewBuffer()
rootCmd *cobra.Command
opt *commonoptions.Common
port int
orasRegistry *remote.Registry
configFile string
err error
args []string
)
func TestPull(t *testing.T) {
RegisterFailHandler(Fail)
port, err = testutils.FreePort()
Expect(err).ToNot(HaveOccurred())
registry = fmt.Sprintf("localhost:%d", port)
RunSpecs(t, "Pull Suite")
}
var _ = BeforeSuite(func() {
config := &configuration.Configuration{}
config.HTTP.Addr = fmt.Sprintf("localhost:%d", port)
// Create and configure the common options.
opt = commonoptions.NewOptions()
opt.Initialize(commonoptions.WithWriter(output))
// Create the oras registry.
orasRegistry, err = testutils.NewOrasRegistry(registry, true)
Expect(err).ToNot(HaveOccurred())
// Start the local registry.
go func() {
err := testutils.StartRegistry(context.Background(), config)
Expect(err).ToNot(BeNil())
}()
// Check that the registry is up and accepting connections.
Eventually(func(g Gomega) error {
res, err := http.Get(fmt.Sprintf("http://%s", config.HTTP.Addr))
g.Expect(err).ShouldNot(HaveOccurred())
g.Expect(res.StatusCode).Should(Equal(http.StatusOK))
return err
}).WithTimeout(time.Second * 5).ShouldNot(HaveOccurred())
// Create temporary directory used to save the configuration file.
configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
Expect(err).Should(Succeed())
})
var _ = AfterSuite(func() {
configDir := filepath.Dir(configFile)
Expect(os.RemoveAll(configDir)).Should(Succeed())
})
//nolint:unused // false positive
func executeRoot(args []string) error {
rootCmd.SetArgs(args)
rootCmd.SetOut(output)
return cmd.Execute(rootCmd, opt)
}

View File

@ -0,0 +1,327 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 pull_test
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
"oras.land/oras-go/v2/registry/remote/auth"
"github.com/falcosecurity/falcoctl/cmd"
"github.com/falcosecurity/falcoctl/pkg/oci"
"github.com/falcosecurity/falcoctl/pkg/oci/authn"
ocipusher "github.com/falcosecurity/falcoctl/pkg/oci/pusher"
out "github.com/falcosecurity/falcoctl/pkg/output"
)
//nolint:lll,unused // no need to check for line length.
var registryPullUsage = `Usage:
falcoctl registry pull hostname/repo[:tag|@digest] [flags]
Flags:
-o, --dest-dir string destination dir where to save the artifacts(default: current directory)
-h, --help help for pull
--plain-http allows interacting with remote registry via plain http requests
--platform stringArray os and architecture of the artifact in OS/ARCH format (only for plugins artifacts)
Global Flags:
--config string config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
--disable-styling Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false)
-v, --verbose Enable verbose logs (default false)
`
//nolint:unused // false positive
var registryPullHelp = `Pull Falco "rulesfile" or "plugin" OCI artifacts from remote registry.
Artifact references are passed as arguments.
A reference is a fully qualified reference ("<registry>/<repository>"),
optionally followed by ":<tag>" (":latest" is assumed by default when no tag is given).
Example - Pull artifact "myplugin" for the platform where falcoctl is running (default) in the current working directory (default):
falcoctl registry pull localhost:5000/myplugin:latest
Example - Pull artifact "myplugin" for platform "linux/arm64" in the current working directory (default):
falcoctl registry pull localhost:5000/myplugin:latest --platform linux/arm64
Example - Pull artifact "myplugin" for platform "linux/arm64" in "myDir" directory:
falcoctl registry pull localhost:5000/myplugin:latest --platform linux/arm64 --dest-dir=./myDir
Example - Pull artifact "myrulesfile":
falcoctl registry pull localhost:5000/myrulesfile:latest
`
//nolint:unused // false positive
var pullAssertFailedBehavior = func(usage, specificError string) {
It("check that fails and the usage is not printed", func() {
Expect(err).To(HaveOccurred())
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(usage)))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(specificError)))
})
}
//nolint:unused // false positive
var registryPullTests = Describe("pull", func() {
var (
pusher *ocipusher.Pusher
ref string
config ocipusher.Option
)
const (
// Used as flags for all the test cases.
registryCmd = "registry"
pullCmd = "pull"
dep1 = "myplugin:1.2.3"
dep2 = "myplugin1:1.2.3|otherplugin:3.2.1"
req = "engine_version:15"
anSource = "myrepo.com/rules.git"
artifact = "generic-repo"
repo = "/" + artifact
tag = "tag"
repoAndTag = repo + ":" + tag
)
// Each test gets its own root command and runs it.
// The err variable is asserted by each test.
JustBeforeEach(func() {
rootCmd = cmd.New(ctx, opt)
err = executeRoot(args)
})
JustAfterEach(func() {
Expect(output.Clear()).ShouldNot(HaveOccurred())
})
Context("help message", func() {
BeforeEach(func() {
args = []string{registryCmd, pullCmd, "--help"}
})
It("should match the saved one", func() {
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(registryPullHelp)))
})
})
// Here we are testing all the failure cases using both the rulesfile and plugin artifact types.
// The common logic for the artifacts is tested once using a rulesfile artifact, no need to repeat
// the same test using a plugin artifact.
Context("failure", func() {
var (
tracker out.Tracker
options []ocipusher.Option
filePathsAndPlatforms ocipusher.Option
destDir string
)
const (
plainHTTP = true
testPluginPlatform1 = "linux/amd64"
)
When("without artifact", func() {
BeforeEach(func() {
args = []string{registryCmd, pullCmd}
})
pullAssertFailedBehavior(registryPullUsage, "ERROR accepts 1 arg(s), received 0")
})
When("unreachable registry", func() {
BeforeEach(func() {
configDir := GinkgoT().TempDir()
configFile := filepath.Join(configDir, ".config")
_, err := os.Create(configFile)
Expect(err).To(BeNil())
args = []string{registryCmd, pullCmd, "noregistry/testrules", "--plain-http", "--config", configFile}
})
pullAssertFailedBehavior(registryPullUsage, "ERROR unable to connect to remote registry")
})
When("invalid repository", func() {
newReg := registry + "/wrong:latest"
BeforeEach(func() {
configDir := GinkgoT().TempDir()
configFile := filepath.Join(configDir, ".config")
_, err := os.Create(configFile)
Expect(err).To(BeNil())
args = []string{registryCmd, pullCmd, newReg, "--plain-http", "--config", configFile}
})
pullAssertFailedBehavior(registryPullUsage, fmt.Sprintf("ERROR %s: not found", newReg))
})
When("unwritable --dest-dir", func() {
BeforeEach(func() {
configDir := GinkgoT().TempDir()
configFile := filepath.Join(configDir, ".config")
_, err := os.Create(configFile)
Expect(err).To(BeNil())
destDir = GinkgoT().TempDir()
err = os.Chmod(destDir, 0o555)
Expect(err).To(BeNil())
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
ref = registry + repoAndTag
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{})
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
options = []ocipusher.Option{filePathsAndPlatforms, config}
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
Expect(err).To(BeNil())
Expect(result).ToNot(BeNil())
args = []string{registryCmd, pullCmd, ref, "--plain-http",
"--platform", testPluginPlatform1, "--dest-dir", destDir,
"--config", configFile,
}
})
It("check that fails and the usage is not printed", func() {
tmp := strings.Split(repoAndTag, "/")
artNameAndTag := tmp[len(tmp)-1]
tmp = strings.Split(artNameAndTag, ":")
artName := tmp[0]
tag := tmp[1]
expectedError := fmt.Sprintf(
"ERROR unable to pull artifact generic-repo with %s tag from repo %s: failed to create file",
tag, artName)
Expect(err).To(HaveOccurred())
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage)))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
})
})
When("--dest-dir not present (and parent not writable)", func() {
BeforeEach(func() {
configDir := GinkgoT().TempDir()
configFile := filepath.Join(configDir, ".config")
_, err := os.Create(configFile)
Expect(err).To(BeNil())
baseDir := GinkgoT().TempDir()
err = os.Chmod(baseDir, 0o555)
Expect(err).To(BeNil())
destDir = baseDir + "/dest"
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
ref = registry + repoAndTag
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{})
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
options = []ocipusher.Option{filePathsAndPlatforms, config}
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
Expect(err).To(BeNil())
Expect(result).ToNot(BeNil())
args = []string{registryCmd, pullCmd, ref, "--plain-http",
"--platform", testPluginPlatform1, "--dest-dir", destDir,
"--config", configFile,
}
})
It("check that fails and the usage is not printed", func() {
expectedError := fmt.Sprintf("ERROR unable to pull artifact %s with tag %s from repo %s: failed to ensure directories of the target path: "+
"mkdir %s: permission denied", artifact, tag, artifact, destDir)
Expect(err).To(HaveOccurred())
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage)))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
})
})
When("wrong digest format", func() {
wrongDigest := "sha256:06f961b802bc46ee168555f066d28f4f0e9afdf3f88174c1ee6f9de004fc30a0"
BeforeEach(func() {
configDir := GinkgoT().TempDir()
configFile := filepath.Join(configDir, ".config")
_, err := os.Create(configFile)
Expect(err).To(BeNil())
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
ref = registry + repoAndTag
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{})
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
options = []ocipusher.Option{filePathsAndPlatforms, config}
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
Expect(err).To(BeNil())
Expect(result).ToNot(BeNil())
ref = registry + repoAndTag + "@" + wrongDigest
args = []string{registryCmd, pullCmd, ref, "--plain-http",
"--platform", testPluginPlatform1, "--config", configFile}
})
It("check that fails and the usage is not printed", func() {
expectedError := fmt.Sprintf("ERROR %s: not found", registry+repo+"@"+wrongDigest)
Expect(err).To(HaveOccurred())
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage)))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
})
})
When("missing repository", func() {
BeforeEach(func() {
configDir := GinkgoT().TempDir()
configFile := filepath.Join(configDir, ".config")
_, err := os.Create(configFile)
Expect(err).To(BeNil())
ref = repoAndTag
args = []string{registryCmd, pullCmd, ref, "--plain-http", "--config", configFile}
})
It("check that fails and the usage is not printed", func() {
expectedError := fmt.Sprintf("ERROR cannot extract registry name from ref %q", ref)
Expect(err).To(HaveOccurred())
Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage)))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
})
})
When("invalid repository", func() {
newReg := registry + "/wrong@something"
BeforeEach(func() {
configDir := GinkgoT().TempDir()
configFile := filepath.Join(configDir, ".config")
_, err := os.Create(configFile)
Expect(err).To(BeNil())
args = []string{registryCmd, pullCmd, newReg, "--plain-http", "--config", configFile}
})
pullAssertFailedBehavior(registryPullUsage, fmt.Sprintf("ERROR unable to create new repository with ref %s: "+
"invalid reference: invalid digest %q: invalid checksum digest format\n", newReg, "something"))
})
When("invalid platform", func() {
BeforeEach(func() {
configDir := GinkgoT().TempDir()
configFile := filepath.Join(configDir, ".config")
_, err := os.Create(configFile)
Expect(err).To(BeNil())
pusher = ocipusher.NewPusher(authn.NewClient(authn.WithCredentials(&auth.EmptyCredential)), plainHTTP, tracker)
ref = registry + repoAndTag
config = ocipusher.WithArtifactConfig(oci.ArtifactConfig{})
filePathsAndPlatforms = ocipusher.WithFilepathsAndPlatforms([]string{plugintgz}, []string{testPluginPlatform1})
options = []ocipusher.Option{filePathsAndPlatforms, config}
result, err := pusher.Push(ctx, oci.Plugin, ref, options...)
Expect(err).To(BeNil())
Expect(result).ToNot(BeNil())
ref = registry + repoAndTag
args = []string{registryCmd, pullCmd, ref, "--plain-http",
"--platform", "linux/unknown", "--config", configFile}
})
pullAssertFailedBehavior(registryPullUsage, "not found: no matching manifest was found in the manifest list")
})
})
})

17
cmd/registry/push/doc.go Normal file
View File

@ -0,0 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 push defines the logic to push local artifacts to a remote repository.
package push

343
cmd/registry/push/push.go Normal file
View File

@ -0,0 +1,343 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2023 The Falco Authors
//
// 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 push
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/blang/semver/v4"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/falcosecurity/falcoctl/internal/utils"
"github.com/falcosecurity/falcoctl/pkg/oci"
ocipusher "github.com/falcosecurity/falcoctl/pkg/oci/pusher"
ociutils "github.com/falcosecurity/falcoctl/pkg/oci/utils"
"github.com/falcosecurity/falcoctl/pkg/options"
"github.com/falcosecurity/falcoctl/pkg/output"
)
const (
longPush = `Push Falco "rulesfile" or "plugin" OCI artifacts to remote registry
Example - Push artifact "myplugin.tar.gz" of type "plugin" for the platform where falcoctl is running (default):
falcoctl registry push --type plugin --version "1.2.3" localhost:5000/myplugin:latest myplugin.tar.gz
Example - Push artifact "myplugin.tar.gz" of type "plugin" for platform "linux/arm64":
falcoctl registry push --type plugin --version "1.2.3" localhost:5000/myplugin:latest myplugin.tar.gz --platform linux/arm64
Example - Push artifact "myplugin.tar.gz" of type "plugin" for multiple platforms:
falcoctl registry push --type plugin --version "1.2.3" localhost:5000/myplugin:latest \
myplugin-linux-x86_64.tar.gz --platform linux/x86_64 \
myplugin-linux-arm64.tar.gz --platform linux/arm64
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile":
falcoctl registry push --type rulesfile --version "0.1.2" localhost:5000/myrulesfile:latest myrulesfile.tar.gz
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile" with floating tags for the major and minor versions (0 and 0.1):
falcoctl registry push --type rulesfile --version "0.1.2" localhost:5000/myrulesfile:latest myrulesfile.tar.gz \
--add-floating-tags
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile" to an insecure registry:
falcoctl registry push --type rulesfile --version "0.1.2" --plain-http localhost:5000/myrulesfile:latest myrulesfile.tar.gz
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile" with a dependency "myplugin:1.2.3":
falcoctl registry push --type rulesfile --version "0.1.2" localhost:5000/myrulesfile:latest myrulesfile.tar.gz \
--depends-on myplugin:1.2.3
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile" with a dependency "myplugin:1.2.3" and an alternative "otherplugin:3.2.1":
falcoctl registry push --type rulesfile --version "0.1.2" localhost:5000/myrulesfile:latest myrulesfile.tar.gz \
--depends-on "myplugin:1.2.3|otherplugin:3.2.1"
Example - Push artifact "myrulesfile.tar.gz" of type "rulesfile" with multiple dependencies "myplugin:1.2.3", "otherplugin:3.2.1":
falcoctl registry push --type rulesfile --version "0.1.2" localhost:5000/myrulesfile:latest myrulesfile.tar.gz \
--depends-on myplugin:1.2.3 \
--depends-on otherplugin:3.2.1
`
)
type pushOptions struct {
*options.Common
*options.Artifact
*options.Registry
}
func (o *pushOptions) validate() error {
return o.Artifact.Validate()
}
// NewPushCmd returns the push command.
func NewPushCmd(ctx context.Context, opt *options.Common) *cobra.Command {
o := pushOptions{
Common: opt,
Artifact: &options.Artifact{},
Registry: &options.Registry{},
}
cmd := &cobra.Command{
Use: "push hostname/repo[:tag|@digest] file [flags]",
DisableFlagsInUseLine: true,
Short: "Push a Falco OCI artifact to remote registry",
Long: longPush,
Args: cobra.MinimumNArgs(2),
PreRunE: func(cmd *cobra.Command, args []string) error {
if err := o.validate(); err != nil {
return err
}
ref := args[0]
_, err := utils.GetRegistryFromRef(ref)
if err != nil {
return err
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.runPush(ctx, args)
},
}
o.Registry.AddFlags(cmd)
output.ExitOnErr(o.Printer, o.Artifact.AddFlags(cmd))
return cmd
}
// runPush executes the business logic for the push command.
func (o *pushOptions) runPush(ctx context.Context, args []string) error {
ref := args[0]
paths := args[1:]
// When creating the tar.gz archives we need to remove them after we are done.
// Holds the path for each temporary dir.
var toBeDeletedTmpDirs []string
logger := o.Printer.Logger
registry, err := utils.GetRegistryFromRef(ref)
if err != nil {
return err
}
pusher, err := ociutils.Pusher(o.PlainHTTP, o.Printer)
if err != nil {
return fmt.Errorf("an error occurred while creating the pusher for registry %s: %w", registry, err)
}
err = ociutils.CheckConnectionForRegistry(ctx, pusher.Client, o.PlainHTTP, registry)
if err != nil {
return err
}
logger.Info("Preparing to push artifact", o.Printer.Logger.Args("name", args[0], "type", o.ArtifactType))
// Make sure to remove temporary working dirs.
defer func() {
for _, dir := range toBeDeletedTmpDirs {
logger.Debug("Removing temporary dir", logger.Args("name", dir))
if err := os.RemoveAll(dir); err != nil {
logger.Warn("Unable to remove temporary dir", logger.Args("name", dir, "error", err.Error()))
}
}
}()
config := &oci.ArtifactConfig{
Name: o.Name,
Version: o.Version,
}
for i, p := range paths {
if err = utils.IsTarGz(filepath.Clean(p)); err != nil && !errors.Is(err, utils.ErrNotTarGz) {
return err
} else if err == nil {
continue
} else {
if o.ArtifactType == oci.Rulesfile {
if config, err = rulesConfigLayer(o.Printer.Logger, p, o.Artifact); err != nil {
return err
}
}
path, err := utils.CreateTarGzArchive("", p, true)
if err != nil {
return err
}
paths[i] = path
toBeDeletedTmpDirs = append(toBeDeletedTmpDirs, filepath.Dir(path))
}
}
if config.Name == "" {
// extract artifact name from ref, if not provided by the user
if config.Name, err = utils.NameFromRef(ref); err != nil {
return err
}
}
if err := config.ParseDependencies(o.Dependencies...); err != nil {
return err
}
if err := config.ParseRequirements(o.Requirements...); err != nil {
return err
}
if o.AutoFloatingTags {
v, err := semver.Parse(o.Version)
if err != nil {
return fmt.Errorf("expected semver for the flag \"--version\": %w", err)
}
o.Tags = append(o.Tags, o.Version, fmt.Sprintf("%v", v.Major), fmt.Sprintf("%v.%v", v.Major, v.Minor))
}
opts := ocipusher.Options{
ocipusher.WithTags(o.Tags...),
ocipusher.WithAnnotationSource(o.AnnotationSource),
ocipusher.WithArtifactConfig(*config),
}
switch o.ArtifactType {
case oci.Plugin:
opts = append(opts, ocipusher.WithFilepathsAndPlatforms(paths, o.Platforms))
case oci.Rulesfile:
opts = append(opts, ocipusher.WithFilepaths(paths))
case oci.Asset:
opts = append(opts, ocipusher.WithFilepaths(paths))
}
res, err := pusher.Push(ctx, o.ArtifactType, ref, opts...)
if err != nil {
return err
}
logger.Info("Artifact pushed", logger.Args("name", args[0], "type", res.Type, "digest", res.RootDigest))
return nil
}
const (
// depsKey is the key for deps in the rulesfiles.
depsKey = "required_plugin_versions"
// engineKey is the key in the rulesfiles.
engineKey = "required_engine_version"
// engineRequirementKey is used as name for the engine requirement in the config layer for the rulesfile artifacts.
engineRequirementKey = "engine_version_semver"
)
func rulesConfigLayer(logger *pterm.Logger, filePath string, artifactOptions *options.Artifact) (*oci.ArtifactConfig, error) {
var data []map[string]interface{}
// Setup OCI artifact configuration
config := oci.ArtifactConfig{
Name: artifactOptions.Name,
Version: artifactOptions.Version,
}
yamlFile, err := os.ReadFile(filepath.Clean(filePath))
if err != nil {
return nil, fmt.Errorf("unable to open rulesfile %s: %w", filePath, err)
}
if err := yaml.Unmarshal(yamlFile, &data); err != nil {
return nil, fmt.Errorf("unable to unmarshal rulesfile %s: %w", filePath, err)
}
// Parse the artifact dependencies.
// Check if the user has provided any.
if len(artifactOptions.Dependencies) != 0 {
logger.Info("Dependencies provided by user", logger.Args("rulesfile", filePath))
if err = config.ParseDependencies(artifactOptions.Dependencies...); err != nil {
return nil, err
}
} else {
// If no user provided then try to parse them from the rulesfile.
var found bool
logger.Info("Parsing dependencies from: ", logger.Args("rulesfile", filePath))
var requiredPluginVersionsEntry interface{}
var ok bool
for _, entry := range data {
if requiredPluginVersionsEntry, ok = entry[depsKey]; !ok {
continue
}
var deps []oci.ArtifactDependency
byteData, err := yaml.Marshal(requiredPluginVersionsEntry)
if err != nil {
return nil, fmt.Errorf("unable to parse dependencies from rulesfile: %w", err)
}
err = yaml.Unmarshal(byteData, &deps)
if err != nil {
return nil, fmt.Errorf("unable to parse dependencies from rulesfile: %w", err)
}
logger.Info("Dependencies correctly parsed from rulesfile")
// Set the deps.
config.Dependencies = deps
found = true
break
}
if !found {
logger.Warn("No dependencies were provided by the user and none were found in the rulesfile.")
}
}
// Parse the requirements.
// Check if the user has provided any.
if len(artifactOptions.Requirements) != 0 {
logger.Info("Requirements provided by user")
if err = config.ParseRequirements(artifactOptions.Requirements...); err != nil {
return nil, err
}
} else {
var found bool
var engineVersion string
logger.Info("Parsing requirements from: ", logger.Args("rulesfile", filePath))
// If no user provided requirements then try to parse them from the rulesfile.
for _, entry := range data {
if requiredEngineVersionEntry, ok := entry[engineKey]; ok {
// Check if the version is an int. This is for backward compatibility. The engine version used to be an
// int but internally used by falco as a semver minor version.
// 15 -> 0.15.0
if engVersionInt, ok := requiredEngineVersionEntry.(int); ok {
engineVersion = fmt.Sprintf("0.%d.0", engVersionInt)
} else {
engineVersion, ok = requiredEngineVersionEntry.(string)
if !ok {
return nil, fmt.Errorf("%s must be an int or a string respecting the semver specification, got type %T", engineKey, requiredEngineVersionEntry)
}
// Check if it is in semver format.
if _, err := semver.Parse(engineVersion); err != nil {
return nil, fmt.Errorf("%s must be in semver format: %w", engineVersion, err)
}
}
// Set the requirements.
config.Requirements = []oci.ArtifactRequirement{{
Name: engineRequirementKey,
Version: engineVersion,
}}
found = true
break
}
}
if !found {
logger.Warn("No requirements were provided by the user and none were found in the rulesfile.")
}
}
return &config, nil
}

View File

@ -0,0 +1,217 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2024 The Falco Authors
//
// 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 push_test
// revive:disable
import (
"fmt"
"os"
"path/filepath"
"regexp"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gbytes"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/falcosecurity/falcoctl/cmd"
"github.com/falcosecurity/falcoctl/internal/utils"
"github.com/falcosecurity/falcoctl/pkg/oci"
testutils "github.com/falcosecurity/falcoctl/pkg/test"
)
// revive:enable
var _ = Describe("pushing plugins", func() {
var (
registryCmd = "registry"
pushCmd = "push"
version = "1.1.1"
// fullRepoName is set each time before each test.
fullRepoName string
// repoName same as fullRepoName.
repoName string
// It is set in the config layer.
artifactNameInConfigLayer = "test-push-plugins"
pushedTags = []string{"tag1", "tag2", "latest"}
// Plugin's platforms.
platformARM64 = "linux/arm64"
platformAMD64 = "linux/amd64"
// Paths pointing to plugins that will be pushed.
// Some of the functions expect these two variable to be set to valid paths.
// They are set in beforeEach blocks by tests that need them.
pluginOne string
pluginTwo string
// Data fetched from registry and used for assertions.
pluginData *testutils.PluginArtifact
)
const (
// Used as flags for all the test cases.
requirement = "plugin_api_version:3.2.1"
anSource = "myrepo.com/rules.git"
pluginsRepoBaseName = "push-plugins-tests"
)
var AssertSuccessBehaviour = func(deps []oci.ArtifactDependency, reqs []oci.ArtifactRequirement, annotations map[string]string, platforms []string) {
It("should succeed", func() {
// We do not check the error here since we are checking it after
// pushing the artifact.
By("checking no error in output")
Expect(output).ShouldNot(gbytes.Say("ERROR"))
Expect(output).ShouldNot(gbytes.Say("Unable to remove temporary dir"))
By("checking descriptor")
Expect(pluginData.Descriptor.MediaType).Should(Equal(v1.MediaTypeImageIndex))
Expect(output).Should(gbytes.Say(regexp.QuoteMeta(pluginData.Descriptor.Digest.String())))
By("checking index")
Expect(pluginData.Index.Manifests).Should(HaveLen(len(platforms)))
By("checking platforms")
for _, p := range platforms {
Expect(pluginData.Platforms).Should(HaveKey(p))
}
By("checking config layers")
for plat, p := range pluginData.Platforms {
By(fmt.Sprintf("platform %s", plat))
Expect(p.Config.Version).Should(Equal(version))
Expect(p.Config.Name).Should(Equal(artifactNameInConfigLayer))
By("checking dependencies")
Expect(p.Config.Dependencies).Should(HaveLen(len(deps)))
for _, dep := range deps {
Expect(p.Config.Dependencies).Should(ContainElement(dep))
}
By("checking requirements")
Expect(p.Config.Requirements).Should(HaveLen(len(reqs)))
for _, req := range reqs {
Expect(p.Config.Requirements).Should(ContainElement(req))
}
By("checking annotations")
// The creation timestamp is always present.
Expect(p.Manifest.Annotations).Should(HaveLen(len(annotations) + 1))
for key, val := range annotations {
Expect(p.Manifest.Annotations).Should(HaveKeyWithValue(key, val))
}
}
By("checking tags")
Expect(pluginData.Tags).Should(HaveLen(len(pushedTags)))
Expect(pluginData.Tags).Should(ContainElements(pushedTags))
By("checking that temporary dirs have been removed")
Eventually(func() bool {
entries, err := os.ReadDir("/tmp")
Expect(err).ShouldNot(HaveOccurred())
for _, e := range entries {
if e.IsDir() {
matched, err := filepath.Match(utils.TmpDirPrefix+"*", regexp.QuoteMeta(e.Name()))
Expect(err).ShouldNot(HaveOccurred())
if matched {
return true
}
}
}
return false
}).WithTimeout(5 * time.Second).Should(BeFalse())
})
}
// Each test gets its own root command and runs it.
// The err variable is asserted by each test.
JustBeforeEach(func() {
rootCmd = cmd.New(ctx, opt)
err = executeRoot(args)
})
JustAfterEach(func() {
// Reset the status after each test.
// This variable could be changed by single tests.
// Make sure to set them at their default values.
Expect(output.Clear()).ShouldNot(HaveOccurred())
artifactNameInConfigLayer = "test-plugin"
pushedTags = []string{"tag1", "tag2", "latest"}
pluginOne = ""
pluginTwo = ""
})
Context("success", func() {
JustBeforeEach(func() {
// Check the returned error before proceeding.
Expect(err).ShouldNot(HaveOccurred())
pluginData, err = testutils.FetchPluginFromRegistry(ctx, repoName, pushedTags[0], orasRegistry)
Expect(err).ShouldNot(HaveOccurred())
})
When("two platforms, with reqs and deps", func() {
BeforeEach(func() {
repoName, fullRepoName = randomRulesRepoName(registry, pluginsRepoBaseName)
pluginOne = rulesfileyaml
pluginTwo = plugintgz
args = []string{registryCmd, pushCmd, fullRepoName, pluginOne, pluginTwo, "--type", "plugin", "--platform",
platformAMD64, "--platform", platformARM64, "--version", version, "--config", configFile,
"--plain-http", "--depends-on", "my-test:4.3.2", "--requires", requirement, "--annotation-source", anSource,
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
})
AssertSuccessBehaviour([]oci.ArtifactDependency{{
Name: "my-test",
Version: "4.3.2",
Alternatives: nil,
}}, []oci.ArtifactRequirement{
{
Name: "plugin_api_version",
Version: "3.2.1",
},
}, map[string]string{
"org.opencontainers.image.source": anSource,
}, []string{
platformAMD64, platformARM64,
})
})
When("one platform, no reqs", func() {
BeforeEach(func() {
repoName, fullRepoName = randomRulesRepoName(registry, pluginsRepoBaseName)
pluginOne = plugintgz
args = []string{registryCmd, pushCmd, fullRepoName, pluginOne, "--type", "plugin", "--platform",
platformAMD64, "--version", version, "--config", configFile,
"--plain-http", "--depends-on", "my-test:4.3.2", "--annotation-source", anSource,
"--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer}
})
// We expect to succeed and that the requirement is empty.
AssertSuccessBehaviour([]oci.ArtifactDependency{{
Name: "my-test",
Version: "4.3.2",
Alternatives: nil,
}}, []oci.ArtifactRequirement{}, map[string]string{
"org.opencontainers.image.source": anSource,
}, []string{
platformAMD64,
})
})
})
})

Some files were not shown because too many files have changed in this diff Show More