Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
db5a09b
feat(scanning): audit GitHub Actions workflows across repositories
nakberli841-bot Jun 26, 2026
664e589
Merge branch 'main' into feat/10-github-actions-workflow-audit
nakberli841-bot Jun 26, 2026
3fdd3f4
fix(backend): merge conflict
nakberli841-bot Jun 26, 2026
86e40b7
Update WorkflowScanRepositoryTest.java
nakberli841-bot Jun 26, 2026
7b87695
Update WorkflowScanRepositoryTest.java
nakberli841-bot Jun 26, 2026
300d4d0
Update WorkflowScanRepositoryTest.java
nakberli841-bot Jun 26, 2026
a2bcfed
fix(worker): add githubclient package to component scan in CleatWorke…
nakberli841-bot Jun 27, 2026
b4786e8
fix: resolve missing repository imports in tests
nakberli841-bot Jun 27, 2026
af8216c
feat: add installationId to AccountEntity and fix migration versioning
nakberli841-bot Jun 27, 2026
1748dc5
feat: add installationId to Account entity and update database schema
nakberli841-bot Jun 27, 2026
a0cbe57
fix: resolve checkstyle violations
nakberli841-bot Jun 27, 2026
7787e1f
fix: resolve CI build failures in test modules
nakberli841-bot Jun 27, 2026
622e666
fix(worker): narrow EntityScan and EnableJpaRepositories to specific …
nakberli841-bot Jun 27, 2026
48c3564
fix(scanning): replace invalid SHA in WorkflowParserTest with valid h…
nakberli841-bot Jun 27, 2026
428728a
fix(github-client): mark redisTemplate bean as @Primary to resolve No…
nakberli841-bot Jun 27, 2026
eca70f6
fix(worker): add test application.yml with required GitHub and scan p…
nakberli841-bot Jun 27, 2026
035e1e2
fix(worker): fix YAML syntax in test application.yml
nakberli841-bot Jun 28, 2026
d4d489b
fix: resolve LazyInitializationException by enabling @Transactional f…
nakberli841-bot Jun 28, 2026
ba5ba02
fix: retrieve workflow content from base64 field to avoid host mismatch
nakberli841-bot Jun 28, 2026
8b59d84
fix: delete old scan records before checking for empty workflow files
nakberli841-bot Jun 28, 2026
269ac17
refactor(scanning): improve permission and OIDC audit logic
nakberli841-bot Jun 28, 2026
e431f7a
refactor(scanning): align test expectations with new OIDC logic
nakberli841-bot Jun 28, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package dev.cleat.api.controller;

import dev.cleat.persistence.entity.WorkflowScanEntity;
import dev.cleat.persistence.repository.WorkflowScanRepository;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/repos")
public class WorkflowScanController {

private final WorkflowScanRepository workflowScanRepository;

public WorkflowScanController(WorkflowScanRepository workflowScanRepository) {
this.workflowScanRepository = workflowScanRepository;
}

@GetMapping("/{repoId}/workflow-scans")
public List<WorkflowScanResponse> getWorkflowScans(@PathVariable UUID repoId) {
return workflowScanRepository.findByRepoIdOrderByScannedAtDesc(repoId).stream()
.map(WorkflowScanResponse::from)
.toList();
}

public record WorkflowScanResponse(
UUID id,
String workflowPath,
int unpinnedActions,
boolean broadPermissions,
boolean missingOidc,
int riskScore,
OffsetDateTime scannedAt) {

public static WorkflowScanResponse from(WorkflowScanEntity entity) {
return new WorkflowScanResponse(
entity.getId(),
entity.getWorkflowPath(),
entity.getUnpinnedActions(),
entity.getBroadPermissions(),
entity.getMissingOidc(),
entity.getRiskScore(),
entity.getScannedAt());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
@SpringBootTest
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class AbstractIntegrationTest {

@Container
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package dev.cleat.api;

import static org.assertj.core.api.Assertions.assertThat;

import dev.cleat.common.enums.AccountType;
import dev.cleat.persistence.entity.AccountEntity;
import dev.cleat.persistence.entity.RepoEntity;
import dev.cleat.persistence.entity.WorkflowScanEntity;
import dev.cleat.persistence.repository.AccountRepository;
import dev.cleat.persistence.repository.RepoRepository;
import dev.cleat.persistence.repository.WorkflowScanRepository;
import java.time.OffsetDateTime;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate;

class WorkflowScanControllerTest extends AbstractIntegrationTest {

@Autowired
private AccountRepository accountRepository;

@Autowired
private RepoRepository repoRepository;

@Autowired
private WorkflowScanRepository workflowScanRepository;

@Autowired
private TestRestTemplate restTemplate;

@Test
void returnsWorkflowScansForRepo() {
AccountEntity account = new AccountEntity()
.setLogin("octocat")
.setName("Octocat")
.setType(AccountType.USER)
.setInstallationId("inst-123");
accountRepository.saveAndFlush(account);

RepoEntity repo = new RepoEntity().setAccount(account).setName("hello-world");
repoRepository.saveAndFlush(repo);

workflowScanRepository.saveAndFlush(new WorkflowScanEntity()
.setRepo(repo)
.setWorkflowPath(".github/workflows/ci.yml")
.setUnpinnedActions(2)
.setBroadPermissions(false)
.setMissingOidc(true)
.setRiskScore(80)
.setScannedAt(OffsetDateTime.now()));

var response = restTemplate
.withBasicAuth("test", "test")
.getForEntity("/api/repos/" + repo.getId() + "/workflow-scans", List.class);

// Assert
assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
assertThat(response.getBody()).hasSize(1);
}

@Test
void returnsEmptyListWhenNoScans() {
AccountEntity account = new AccountEntity()
.setLogin("octocat3")
.setName("Octocat3")
.setType(AccountType.USER)
.setInstallationId("inst-456");
accountRepository.saveAndFlush(account);

RepoEntity repo = new RepoEntity().setAccount(account).setName("empty-repo");
repoRepository.saveAndFlush(repo);

var response = restTemplate
.withBasicAuth("test", "test")
.getForEntity("/api/repos/" + repo.getId() + "/workflow-scans", List.class);

assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
assertThat(response.getBody()).isEmpty();
}
}
5 changes: 5 additions & 0 deletions backend/apps/api/src/test/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
spring:
security:
user:
name: test
password: test
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
@EntityScan(basePackages = "dev.cleat.persistence.entity")
@EnableJpaRepositories(basePackages = "dev.cleat.persistence.repository")
@ComponentScan(basePackages = {"dev.cleat.worker", "dev.cleat.scanning", "dev.cleat.githubclient"})
public class CleatWorkerApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package dev.cleat.worker;

import dev.cleat.persistence.entity.RepoEntity;
import dev.cleat.persistence.repository.AccountRepository;
import dev.cleat.scanning.WorkflowScanService;
import jakarta.transaction.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class WorkflowScanJob {

private static final Logger LOG = LoggerFactory.getLogger(WorkflowScanJob.class);

private final AccountRepository accountRepository;
private final WorkflowScanService workflowScanService;

public WorkflowScanJob(AccountRepository accountRepository, WorkflowScanService workflowScanService) {
this.accountRepository = accountRepository;
this.workflowScanService = workflowScanService;
}

@Scheduled(fixedDelayString = "${cleat.workflow-scan.interval-ms:3600000}")
@Transactional
public void run() {
LOG.info("Starting workflow scan job");

accountRepository.findAll().forEach(account -> {
String installationId = account.getInstallationId();

for (RepoEntity repo : account.getRepos()) {
Comment thread
nakberli841-bot marked this conversation as resolved.
try {
workflowScanService.scanRepo(repo, installationId);
} catch (Exception e) {
LOG.error("Failed to scan workflows for repo {}/{}", account.getLogin(), repo.getName(), e);
}
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

LOG.info("Workflow scan job completed");
}
}
5 changes: 4 additions & 1 deletion backend/apps/worker/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ management:
db:
enabled: true
redis:
enabled: true
enabled: true
cleat:
workflow-scan:
interval-ms: 3600000
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package dev.cleat.worker;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
@SpringBootTest
public abstract class AbstractIntegrationTest {

@Container
Expand Down
7 changes: 7 additions & 0 deletions backend/apps/worker/src/test/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
github:
app-id: "test-app-id"
private-key-path: "/tmp/test-key.pem"

cleat:
workflow-scan:
interval-ms: 3600000
Comment on lines +5 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd 'CleatWorkerApplication\.java$' backend/apps/worker/src/main/java -x sh -c '
  echo "== $1 ==";
  ast-grep outline "$1";
  sed -n "1,160p" "$1"
' sh {}

fd 'AbstractIntegrationTest\.java$' backend/apps/worker/src/test/java -x sh -c '
  echo "== $1 ==";
  ast-grep outline "$1";
  sed -n "1,220p" "$1"
' sh {}

rg -n -C2 '`@EnableScheduling`|`@Scheduled`|spring\.task\.scheduling\.enabled|workflow-scan\.interval-ms' backend/apps/worker

Repository: Devlaner/cleat

Length of output: 3224


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== test resources =="
git ls-files 'backend/apps/worker/src/test/resources/**' | sort

echo
echo "== scheduling-related config in worker tests =="
rg -n -C2 'spring\.task\.scheduling\.enabled|EnableScheduling|`@Scheduled`|workflow-scan\.interval-ms' \
  backend/apps/worker/src/test backend/apps/worker/src/main

echo
echo "== worker test classes =="
fd -e java backend/apps/worker/src/test/java -x sh -c 'echo "--- $1 ---"; sed -n "1,220p" "$1"' sh {}

Repository: Devlaner/cleat

Length of output: 2311


Disable the scheduler in worker tests
CleatWorkerApplication enables scheduling globally, and this test config leaves cleat.workflow-scan.interval-ms active, so WorkflowScanJob can run when the test context starts. Add spring.task.scheduling.enabled: false here to keep integration tests deterministic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/apps/worker/src/test/resources/application.yml` around lines 5 - 7,
Disable scheduling in the worker test configuration so `WorkflowScanJob` does
not run during test startup. Update the test `application.yml` used by
`CleatWorkerApplication` to set `spring.task.scheduling.enabled` to false while
keeping the existing `cleat.workflow-scan.interval-ms` settings as-is. This
change should be made in the test resource config that defines the worker
application properties.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
Expand All @@ -16,6 +17,7 @@ public WebClient gitHubWebClient(WebClient.Builder builder) {
}

@Bean
@Primary
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ public class AccountEntity {
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

@Column(name = "installation_id")
private String installationId;

@Column(name = "login", nullable = false)
private String login;

Expand Down Expand Up @@ -199,4 +202,13 @@ public AccountEntity setRepos(List<RepoEntity> repos) {
this.repos = repos;
return this;
}

public String getInstallationId() {
return installationId;
}

public AccountEntity setInstallationId(String installationId) {
this.installationId = installationId;
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ public RepoEntity(
this.staleBranches = staleBranches;
this.openPRs = openPRs;
this.hygieneScore = hygieneScore;

this.scorecard = scorecard;
this.topics = topics;
this.createdAt = createdAt;
Expand Down
Loading
Loading