Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Integration Test

on:
workflow_dispatch:
pull_request:
paths:
- 'iac/**'
- '.github/workflows/integration-test.yml'

permissions:
contents: read

jobs:
tofu-plan:
runs-on: ubuntu-latest
defaults:
run:
working-directory: iac
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Set up Tofu
uses: opentofu/setup-opentofu@v1
Copy link

@kusari-inspector kusari-inspector bot Sep 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Pin the GitHub action to a specific commit hash instead of using a mutable tag reference for better security and reproducibility.

Recommended Code Changes:

uses: opentofu/setup-opentofu@<commit-hash>  # Replace with actual commit hash


- name: Initialize Tofu
run: tofu init

- name: Run Tofu Plan
id: plan
run: |
tofu plan -detailed-exitcode -no-color > plan.txt
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


- name: Check for changes
run: |
if [ "${{ steps.plan.outcome }}" != "success" ]; then
echo "Tofu plan detected changes or failed."
cat plan.txt
exit 1
fi
shell: bash
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ config.yml
output

# go test coverage output
coverage.out
coverage.out

.terraform/
61 changes: 55 additions & 6 deletions data/repository_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ type RepositoryMetadata interface {
IsPublic() bool
OrganizationBlogURL() *string
IsMFARequiredForAdministrativeActions() *bool
IsDefaultBranchProtected() *bool
DefaultBranchRequiresPRReviews() *bool
IsDefaultBranchProtectedFromDeletion() *bool
}

type GitHubRepositoryMetadata struct {
Releases []ReleaseData
Rulesets []Ruleset
ghRepo *github.Repository
ghOrg *github.Organization
Releases []ReleaseData
defaultBranchRules *github.BranchRules
ghRepo *github.Repository
ghOrg *github.Organization
}

func (r *GitHubRepositoryMetadata) IsActive() bool {
Expand All @@ -28,6 +31,30 @@ func (r *GitHubRepositoryMetadata) IsPublic() bool {
return !r.ghRepo.GetPrivate()
}

func (r *GitHubRepositoryMetadata) IsDefaultBranchProtected() *bool {
if r.defaultBranchRules == nil {
return nil
}
updateBlockedByRule := r.defaultBranchRules != nil && len(r.defaultBranchRules.Update) > 0
return &updateBlockedByRule
}

func (r *GitHubRepositoryMetadata) IsDefaultBranchProtectedFromDeletion() *bool {
if r.defaultBranchRules == nil {
return nil
}
deletionBlockedByRule := r.defaultBranchRules != nil && len(r.defaultBranchRules.Deletion) > 0
return &deletionBlockedByRule
}

func (r *GitHubRepositoryMetadata) DefaultBranchRequiresPRReviews() *bool {
if r.defaultBranchRules == nil {
return nil
}
requiresReviews := r.defaultBranchRules != nil && r.defaultBranchRules.PullRequest != nil && len(r.defaultBranchRules.PullRequest) > 0 && r.defaultBranchRules.PullRequest[0].Parameters.RequiredApprovingReviewCount > 0
return &requiresReviews
}

func (r *GitHubRepositoryMetadata) OrganizationBlogURL() *string {
if r.ghOrg != nil {
return r.ghOrg.Blog
Expand All @@ -53,8 +80,30 @@ func loadRepositoryMetadata(ghClient *github.Client, owner, repo string) (ghRepo
ghRepo: repository,
}, nil
}
branchRules, err := getRuleset(ghClient, owner, repo, repository.GetDefaultBranch())
if err != nil {
return repository, &GitHubRepositoryMetadata{
ghRepo: repository,
ghOrg: organization,
}, nil
}
return repository, &GitHubRepositoryMetadata{
ghRepo: repository,
ghOrg: organization,
ghRepo: repository,
ghOrg: organization,
defaultBranchRules: branchRules,
}, nil
}

func getRuleset(ghClient *github.Client, owner, repo string, branchName string) (*github.BranchRules, error) {
branchRules, _, err := ghClient.Repositories.GetRulesForBranch(
context.Background(),
owner,
repo,
branchName,
nil,
)
if err != nil {
return nil, err
}
return branchRules, nil
}
4 changes: 2 additions & 2 deletions evaluation_plans/osps/access_control/evaluations.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func OSPS_AC_03() (evaluation *layer4.ControlEvaluation) {
"Maturity Level 3",
},
[]layer4.AssessmentStep{
branchProtectionRestrictsPushes, // This checks branch protection, but not rulesets yet
defaultBranchRestrictsPushes,
},
)

Expand All @@ -77,7 +77,7 @@ func OSPS_AC_03() (evaluation *layer4.ControlEvaluation) {
"Maturity Level 3",
},
[]layer4.AssessmentStep{
branchProtectionPreventsDeletion, // This checks branch protection, but not rulesets yet
defaultBranchPreventsDeletion,
},
)

Expand Down
34 changes: 24 additions & 10 deletions evaluation_plans/osps/access_control/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func orgRequiresMFA(payloadData any, _ map[string]*layer4.Change) (result layer4
return layer4.Failed, "Two-factor authentication is NOT configured as required by the parent organization"
}

func branchProtectionRestrictsPushes(payloadData any, _ map[string]*layer4.Change) (result layer4.Result, message string) {
func defaultBranchRestrictsPushes(payloadData any, _ map[string]*layer4.Change) (result layer4.Result, message string) {
payload, message := reusable_steps.VerifyPayload(payloadData)
if message != "" {
return layer4.Unknown, message
Expand All @@ -36,28 +36,42 @@ func branchProtectionRestrictsPushes(payloadData any, _ map[string]*layer4.Chang
result = layer4.Passed
message = "Branch protection rule requires approving reviews"
} else {
result = layer4.NeedsReview
message = "Branch protection rule does not restrict pushes or require approving reviews; Rulesets not yet evaluated."
if payload.RepositoryMetadata.IsDefaultBranchProtected() != nil && *payload.RepositoryMetadata.IsDefaultBranchProtected() {
result = layer4.Passed
message = "Branch rule restricts pushes"
} else if payload.RepositoryMetadata.DefaultBranchRequiresPRReviews() != nil && *payload.RepositoryMetadata.DefaultBranchRequiresPRReviews() {
result = layer4.Passed
message = "Branch rule requires approving reviews"
} else {
result = layer4.Failed
message = "Default branch is not protected"
}
}
return
return result, message
}

func branchProtectionPreventsDeletion(payloadData any, _ map[string]*layer4.Change) (result layer4.Result, message string) {
func defaultBranchPreventsDeletion(payloadData any, _ map[string]*layer4.Change) (result layer4.Result, message string) {
payload, message := reusable_steps.VerifyPayload(payloadData)
if message != "" {
return layer4.Unknown, message
}

allowsDeletion := payload.Repository.DefaultBranchRef.RefUpdateRule.AllowsDeletions
branchProtectionAllowsDeletion := payload.Repository.DefaultBranchRef.RefUpdateRule.AllowsDeletions
deletionRule := payload.RepositoryMetadata.IsDefaultBranchProtectedFromDeletion()
branchRulesAllowDeletion := deletionRule == nil || !*deletionRule

if allowsDeletion {
if branchProtectionAllowsDeletion && branchRulesAllowDeletion {
result = layer4.Failed
message = "Branch protection rule allows deletions"
message = "Default branch is not protected from deletions"
} else {
result = layer4.Passed
message = "Branch protection rule prevents deletions"
if *deletionRule {
message = "Default branch is protected from deletions by rulesets"
} else {
message = "Default branch is protected from deletions by branch protection rules"
}
}
return
return result, message
}

func workflowDefaultReadPermissions(payloadData any, _ map[string]*layer4.Change) (result layer4.Result, message string) {
Expand Down
27 changes: 27 additions & 0 deletions iac/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions iac/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Example README for OpenTofu-managed repository

This repository is managed by OpenTofu (Terraform alternative) via Infrastructure as Code.

- Repository: revanite-io/example-osps-baseline-level-1
- Managed resources: repository settings, topics, README file

Feel free to update this file as needed.
12 changes: 12 additions & 0 deletions iac/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
terraform {
required_providers {
github = {
source = "integrations/github"
version = ">= 5.0.0"
}
}
}

provider "github" {
owner = "revanite-io"
}
36 changes: 36 additions & 0 deletions iac/repo.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# OpenTofu configuration for managing the revanite-io/example-osps-baseline-level-1 repository

resource "github_repository" "example_osps_baseline_level_1" {
name = "example-osps-baseline-level-1"
description = "Example repository for integration testing of pvtr-github-repo"
visibility = "public"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Consider if the repository truly needs to be public. If this is intentional for the example repository, you can suppress this warning. If not, change visibility to private.

Recommended Code Changes:

visibility = "private"  # Change from "public" if not intentionally public

has_issues = true
has_wiki = true
has_projects = true
has_downloads = true
vulnerability_alerts = true
}

resource "github_repository_ruleset" "default_branch_protection" {
name = "default"
repository = github_repository.example_osps_baseline_level_1.name
target = "branch"
enforcement = "active"

conditions {
ref_name {
include = ["~DEFAULT_BRANCH"]
exclude = []
}
}

rules {
creation = false
update = true
deletion = true
non_fast_forward = true
pull_request {
required_approving_review_count = 1
}
}
}
1 change: 1 addition & 0 deletions iac/terraform.tfstate
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"version":4,"terraform_version":"1.10.6","serial":2,"lineage":"7bc637ea-e7ff-1f51-242c-395719e69890","outputs":{},"resources":[{"mode":"managed","type":"github_repository","name":"example_osps_baseline_level_1","provider":"provider[\"registry.opentofu.org/integrations/github\"]","instances":[{"schema_version":1,"attributes":{"allow_auto_merge":false,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_squash_merge":true,"allow_update_branch":false,"archive_on_destroy":null,"archived":false,"auto_init":false,"default_branch":"main","delete_branch_on_merge":false,"description":"Example repository for integration testing of pvtr-github-repo","etag":"W/\"a18fbc4b4371fb3fd92f89c85eff300333be99f12c796276ec71f4a170512568\"","full_name":"revanite-io/example-osps-baseline-level-1","git_clone_url":"git://github.com/revanite-io/example-osps-baseline-level-1.git","gitignore_template":null,"has_discussions":false,"has_downloads":true,"has_issues":true,"has_projects":true,"has_wiki":true,"homepage_url":"","html_url":"https://github.com/revanite-io/example-osps-baseline-level-1","http_clone_url":"https://github.com/revanite-io/example-osps-baseline-level-1.git","id":"example-osps-baseline-level-1","ignore_vulnerability_alerts_during_read":null,"is_template":false,"license_template":null,"merge_commit_message":"PR_TITLE","merge_commit_title":"MERGE_MESSAGE","name":"example-osps-baseline-level-1","node_id":"R_kgDOP0IKkg","pages":[],"primary_language":"Python","private":false,"repo_id":1061292690,"security_and_analysis":[{"advanced_security":[],"secret_scanning":[{"status":"disabled"}],"secret_scanning_push_protection":[{"status":"disabled"}]}],"squash_merge_commit_message":"COMMIT_MESSAGES","squash_merge_commit_title":"COMMIT_OR_PR_TITLE","ssh_clone_url":"[email protected]:revanite-io/example-osps-baseline-level-1.git","svn_url":"https://github.com/revanite-io/example-osps-baseline-level-1","template":[],"topics":[],"visibility":"public","vulnerability_alerts":true,"web_commit_signoff_required":false},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="}]},{"mode":"managed","type":"github_repository_ruleset","name":"default_branch_protection","provider":"provider[\"registry.opentofu.org/integrations/github\"]","instances":[{"schema_version":1,"attributes":{"bypass_actors":[],"conditions":[{"ref_name":[{"exclude":[],"include":["~DEFAULT_BRANCH"]}]}],"enforcement":"active","etag":"W/\"4354911cc42f114f622f032d1fedae8c238317d7459bfaca137e33e449db18ab\"","id":"8281062","name":"default","node_id":"RRS_lACqUmVwb3NpdG9yec4_QgqSzgB-W-Y","repository":"example-osps-baseline-level-1","rules":[{"branch_name_pattern":[],"commit_author_email_pattern":[],"commit_message_pattern":[],"committer_email_pattern":[],"creation":false,"deletion":true,"merge_queue":[],"non_fast_forward":true,"pull_request":[{"dismiss_stale_reviews_on_push":false,"require_code_owner_review":false,"require_last_push_approval":false,"required_approving_review_count":1,"required_review_thread_resolution":false}],"required_code_scanning":[],"required_deployments":[],"required_linear_history":false,"required_signatures":false,"required_status_checks":[],"tag_name_pattern":[],"update":true,"update_allows_fetch_and_merge":false}],"ruleset_id":8281062,"target":"branch"},"sensitive_attributes":[],"private":"eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ=="}]}],"check_results":null}
Copy link

@kusari-inspector kusari-inspector bot Sep 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Remove terraform.tfstate from version control entirely. Add terraform.tfstate and terraform.tfstate.backup to .gitignore file. Use remote state storage (like Terraform Cloud, AWS S3, or Azure Storage) instead of committing state files.

Recommended Code Changes:

# Add to .gitignore:
terraform.tfstate
terraform.tfstate.backup
*.tfstate
*.tfstate.*

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenTofu docs claim that because the GH creds for the provider are provided using an environment variable that the value will never be stored in the local state, see https://opentofu.org/docs/language/settings/backends/configuration/#credentials-and-sensitive-data

Loading