-
Notifications
You must be signed in to change notification settings - Fork 5
feat(scanning): audit GitHub Actions workflows across repositories #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
db5a09b
664e589
3fdd3f4
86e40b7
7b87695
300d4d0
a2bcfed
b4786e8
af8216c
1748dc5
a0cbe57
7787e1f
622e666
48c3564
428728a
eca70f6
035e1e2
d4d489b
ba5ba02
8b59d84
269ac17
e431f7a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
|---|---|---|
| @@ -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(); | ||
| } | ||
| } |
| 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 |
|---|---|---|
| @@ -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()) { | ||
| try { | ||
| workflowScanService.scanRepo(repo, installationId); | ||
| } catch (Exception e) { | ||
| LOG.error("Failed to scan workflows for repo {}/{}", account.getLogin(), repo.getName(), e); | ||
| } | ||
| } | ||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| LOG.info("Workflow scan job completed"); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 |
|---|---|---|
| @@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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/workerRepository: 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 🤖 Prompt for AI Agents |
||
Uh oh!
There was an error while loading. Please reload this page.