skootrs_lib/service/
facet.rs

1//
2// Copyright 2024 The Skootrs Authors.
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16// TODO: The content should be templatized or at least kept in separate files as opposed to just
17// being thrown in giant strings inline with the code.
18
19// TODO: Most of the generators for files need to be parameterized better.
20
21#![allow(clippy::module_name_repetitions)]
22#![allow(clippy::unused_self)]
23
24use std::str::FromStr;
25
26use askama::Template;
27use chrono::Datelike;
28
29use tracing::info;
30
31use crate::service::source::SourceService;
32use skootrs_model::{
33    security_insights::insights10::{
34        SecurityInsightsVersion100YamlSchema,
35        SecurityInsightsVersion100YamlSchemaContributionPolicy,
36        SecurityInsightsVersion100YamlSchemaDependencies,
37        SecurityInsightsVersion100YamlSchemaDependenciesSbomItem,
38        SecurityInsightsVersion100YamlSchemaDependenciesSbomItemSbomCreation,
39        SecurityInsightsVersion100YamlSchemaHeader,
40        SecurityInsightsVersion100YamlSchemaHeaderSchemaVersion,
41        SecurityInsightsVersion100YamlSchemaProjectLifecycle,
42        SecurityInsightsVersion100YamlSchemaProjectLifecycleStatus,
43        SecurityInsightsVersion100YamlSchemaVulnerabilityReporting,
44    },
45    skootrs::{
46        facet::{
47            APIBundleFacet, APIBundleFacetParams, APIContent, CommonFacetCreateParams,
48            FacetCreateParams, FacetSetCreateParams, InitializedFacet, SourceBundleFacet,
49            SourceBundleFacetCreateParams, SourceFile, SourceFileContent, SupportedFacetType,
50        },
51        label::Label,
52        InitializedEcosystem, InitializedGithubRepo, InitializedRepo, SkootError,
53    },
54};
55
56use super::source::LocalSourceService;
57
58/// The `LocalFacetService` struct represents a service for creating and managing facets on the local machine.
59#[derive(Debug)]
60pub struct LocalFacetService {}
61
62/// The `RootFacetService` trait provides an interface for initializing and managing a project's facets.
63/// This includes things like initializing and managing source files, source bundles, and API bundles.
64/// It is the root service for all facets and handles which other services to delegate to.
65pub trait RootFacetService {
66    fn initialize(
67        &self,
68        params: FacetCreateParams,
69    ) -> impl std::future::Future<Output = Result<InitializedFacet, SkootError>> + Send;
70    fn initialize_all(
71        &self,
72        params: FacetSetCreateParams,
73    ) -> impl std::future::Future<Output = Result<Vec<InitializedFacet>, SkootError>> + Send;
74}
75
76/// The `SourceBundleFacetService` trait provides an interface for initializing and managing a project's source
77/// bundle facets. This includes things like initializing and managing set of files.
78///
79/// This replaces the `SourceFileFacetService` trait since it's more generic and can handle more than just
80/// single files.
81pub trait SourceBundleFacetService {
82    /// Initializes a source bundle facet.
83    ///
84    /// # Errors
85    ///
86    /// Returns an error if the source bundle facet can't be initialized.
87    fn initialize(
88        &self,
89        params: SourceBundleFacetCreateParams,
90    ) -> Result<SourceBundleFacet, SkootError>;
91}
92
93impl SourceBundleFacetService for LocalFacetService {
94    /// Initializes a source bundle facet.
95    ///
96    /// # Errors
97    ///
98    /// Returns an error if the source bundle facet can't be initialized.
99    fn initialize(
100        &self,
101        params: SourceBundleFacetCreateParams,
102    ) -> Result<SourceBundleFacet, SkootError> {
103        let source_service = LocalSourceService {};
104        let default_source_bundle_content_handler = DefaultSourceBundleContentHandler {};
105        // TODO: Update this to be more generic on the repo service
106        let language_specific_source_bundle_content_handler = match params.common.ecosystem {
107            InitializedEcosystem::Go(_) => GoGithubSourceBundleContentHandler {},
108            InitializedEcosystem::Maven(_) => todo!(),
109        };
110
111        let source_bundle_content = match params.facet_type {
112            SupportedFacetType::Readme
113            | SupportedFacetType::License
114            | SupportedFacetType::SecurityPolicy
115            | SupportedFacetType::Scorecard
116            | SupportedFacetType::SecurityInsights => {
117                default_source_bundle_content_handler.generate_content(&params)?
118            }
119            SupportedFacetType::Gitignore
120            | SupportedFacetType::SLSABuild
121            | SupportedFacetType::DependencyUpdateTool => {
122                language_specific_source_bundle_content_handler.generate_content(&params)?
123            }
124            SupportedFacetType::SBOMGenerator => todo!(),
125            SupportedFacetType::StaticCodeAnalysis => todo!(),
126            SupportedFacetType::BranchProtection => todo!(),
127            SupportedFacetType::CodeReview => todo!(),
128            SupportedFacetType::Fuzzing => {
129                language_specific_source_bundle_content_handler.generate_content(&params)?
130            }
131            SupportedFacetType::PublishPackages => todo!(),
132            SupportedFacetType::PinnedDependencies => todo!(),
133            SupportedFacetType::SAST => {
134                default_source_bundle_content_handler.generate_content(&params)?
135            }
136            SupportedFacetType::VulnerabilityScanner => todo!(),
137            SupportedFacetType::GUACForwardingConfig => todo!(),
138            SupportedFacetType::Allstar => todo!(),
139            SupportedFacetType::DefaultSourceCode => {
140                language_specific_source_bundle_content_handler.generate_content(&params)?
141            }
142            SupportedFacetType::VulnerabilityReporting => {
143                unimplemented!("VulnerabilityReporting is not implemented for source bundles")
144            }
145            SupportedFacetType::Other => todo!(),
146        };
147
148        for source_file_content in &source_bundle_content.source_files_content {
149            info!(
150                "Starting to write file {} to {}",
151                source_file_content.name, source_file_content.path
152            );
153            source_service.write_file(
154                params.common.source.clone(),
155                source_file_content.path.clone(),
156                source_file_content.name.clone(),
157                source_file_content.content.clone(),
158            )?;
159        }
160
161        let source_files: Vec<SourceFile> = source_bundle_content
162            .source_files_content
163            .iter()
164            .map(|source_file_content| {
165                Ok::<SourceFile, SkootError>(SourceFile {
166                    name: source_file_content.name.clone(),
167                    path: source_file_content.path.clone(),
168                    hash: source_service.hash_file(
169                        &params.common.source,
170                        source_file_content.path.clone(),
171                        source_file_content.name.clone(),
172                    )?,
173                })
174            })
175            .collect::<Result<Vec<_>, _>>()?;
176
177        let source_bundle_facet = SourceBundleFacet {
178            source_files: Some(source_files),
179            facet_type: params.facet_type,
180            source_files_content: None,
181            labels: params.labels,
182        };
183
184        Ok(source_bundle_facet)
185    }
186}
187
188/// The `APIBundleFacetService` trait provides an interface for initializing and managing a project's API
189/// bundle facets. This includes things like initializing and managing API calls to services like Github.
190///
191/// These API calls are used to enable features like branch protection, vulnerability reporting, etc.
192pub trait APIBundleFacetService {
193    fn initialize(
194        &self,
195        params: APIBundleFacetParams,
196    ) -> impl std::future::Future<Output = Result<APIBundleFacet, SkootError>> + Send;
197}
198
199impl APIBundleFacetService for LocalFacetService {
200    async fn initialize(&self, params: APIBundleFacetParams) -> Result<APIBundleFacet, SkootError> {
201        // TODO: This should support more than just Github
202        match params.facet_type {
203            SupportedFacetType::CodeReview
204            | SupportedFacetType::BranchProtection
205            | SupportedFacetType::VulnerabilityReporting => {
206                let github_api_bundle_handler = GithubAPIBundleHandler {};
207                let api_bundle_facet = github_api_bundle_handler.generate(&params).await?;
208                Ok(api_bundle_facet)
209            }
210            _ => todo!("Not implemented yet"),
211        }
212    }
213}
214
215/// The `SourceBundleContent` struct represents the content of a set of source files.
216pub struct SourceBundleContent {
217    pub source_files_content: Vec<SourceFileContent>,
218    pub facet_type: SupportedFacetType,
219}
220
221impl RootFacetService for LocalFacetService {
222    async fn initialize(&self, params: FacetCreateParams) -> Result<InitializedFacet, SkootError> {
223        match params {
224            FacetCreateParams::SourceBundle(params) => {
225                let source_bundle_facet = SourceBundleFacetService::initialize(self, params)?;
226                Ok(InitializedFacet::SourceBundle(source_bundle_facet))
227            }
228            FacetCreateParams::APIBundle(params) => {
229                let api_bundle_facet = APIBundleFacetService::initialize(self, params).await?;
230                Ok(InitializedFacet::APIBundle(api_bundle_facet))
231            }
232        }
233    }
234
235    async fn initialize_all(
236        &self,
237        params: FacetSetCreateParams,
238    ) -> Result<Vec<InitializedFacet>, SkootError> {
239        let futures = params
240            .facets_params
241            .iter()
242            .map(move |params| RootFacetService::initialize(self, params.clone()));
243
244        let results = futures::future::try_join_all(futures).await?;
245        Ok(results)
246    }
247}
248
249/// The `APIBundleHandler` trait provides an interface for generating an `APIBundleFacet`.
250/// This includes calling APIs to services like Github to enable features like branch protection,
251/// vulnerability reporting, etc.
252trait APIBundleHandler {
253    async fn generate(&self, params: &APIBundleFacetParams) -> Result<APIBundleFacet, SkootError>;
254}
255
256/// The `GithubAPIBundleHandler` struct represents a handler for generating an `APIBundleFacet` related to
257/// API calls made to Github.
258struct GithubAPIBundleHandler {}
259
260impl APIBundleHandler for GithubAPIBundleHandler {
261    async fn generate(&self, params: &APIBundleFacetParams) -> Result<APIBundleFacet, SkootError> {
262        let InitializedRepo::Github(repo) = &params.common.repo;
263        match params.facet_type {
264            SupportedFacetType::BranchProtection => self.generate_branch_protection(repo).await,
265            SupportedFacetType::VulnerabilityReporting => {
266                self.generate_vulnerability_reporting(repo).await
267            }
268            _ => todo!("Not implemented yet"),
269        }
270    }
271}
272
273impl GithubAPIBundleHandler {
274    async fn generate_branch_protection(
275        &self,
276        repo: &InitializedGithubRepo,
277    ) -> Result<APIBundleFacet, SkootError> {
278        let enforce_branch_protection_endpoint = format!(
279            "/repos/{owner}/{repo}/branches/{branch}/protection",
280            owner = repo.organization.get_name(),
281            repo = repo.name,
282            branch = "main",
283        );
284        info!(
285            "Enabling branch protection for {}",
286            enforce_branch_protection_endpoint
287        );
288        // TODO: This should be a struct that serializes to json instead of just json directly
289        let enforce_branch_protection_body = serde_json::json!({
290            "enforce_admins": true,
291            "required_pull_request_reviews": null,
292            "required_status_checks": null,
293            "restrictions": null,
294            "required_linear_history": true,
295            "allow_force_pushes": false,
296            "allow_deletions": null,
297        });
298
299        // FIXME: I don't quite know why in some cases octocrab loses my auth and I have to re-authenticate
300        let o: octocrab::Octocrab = octocrab::Octocrab::builder()
301            .personal_token(
302                std::env::var("GITHUB_TOKEN").expect("GITHUB_TOKEN env var must be populated"),
303            )
304            .build()?;
305        octocrab::initialise(o);
306        let response: serde_json::Value = octocrab::instance()
307            .put(
308                &enforce_branch_protection_endpoint,
309                Some(&enforce_branch_protection_body),
310            )
311            .await?;
312
313        let apis = vec![APIContent {
314            name: "Enforce Branch Protection".to_string(),
315            url: enforce_branch_protection_endpoint,
316            response: serde_json::to_string_pretty(&response)?,
317        }];
318
319        Ok(APIBundleFacet {
320            facet_type: SupportedFacetType::BranchProtection,
321            apis,
322            labels: vec![],
323        })
324    }
325
326    async fn generate_vulnerability_reporting(
327        &self,
328        repo: &InitializedGithubRepo,
329    ) -> Result<APIBundleFacet, SkootError> {
330        let vulnerability_reporting_endpoint = format!(
331            "/repos/{owner}/{repo}/private-vulnerability-reporting",
332            owner = repo.organization.get_name(),
333            repo = repo.name,
334        );
335        info!(
336            "Enabling vulnerability reporting for {}",
337            &vulnerability_reporting_endpoint
338        );
339        // Note: This call just returns a status with no JSON output also the normal .put I think expects json
340        // output and will fail.
341        octocrab::instance()
342            ._put(&vulnerability_reporting_endpoint, None::<&()>)
343            .await?;
344        let apis = vec![APIContent {
345            name: "Enabling vulnerability reporting".to_string(),
346            url: vulnerability_reporting_endpoint.clone(),
347            response: "Success".to_string(),
348        }];
349        info!(
350            "Vulnerability reporting enabled for {}",
351            &vulnerability_reporting_endpoint
352        );
353
354        Ok(APIBundleFacet {
355            facet_type: SupportedFacetType::VulnerabilityReporting,
356            apis,
357            labels: vec![],
358        })
359    }
360}
361
362/// The `SourceBundleContentGenerator` trait provides an interface for generating the
363/// content (i.e. text) for a set of source files.
364trait SourceBundleContentGenerator {
365    fn generate_content(
366        &self,
367        params: &SourceBundleFacetCreateParams,
368    ) -> Result<SourceBundleContent, SkootError>;
369}
370
371/// Handles the generation of source files content that are generic to all projects by default,
372/// e.g. README.md, LICENSE, etc.
373struct DefaultSourceBundleContentHandler {}
374
375impl SourceBundleContentGenerator for DefaultSourceBundleContentHandler {
376    fn generate_content(
377        &self,
378        params: &SourceBundleFacetCreateParams,
379    ) -> Result<SourceBundleContent, SkootError> {
380        match params.facet_type {
381            SupportedFacetType::Readme => self.generate_readme_content(params),
382            SupportedFacetType::License => self.generate_license_content(params),
383            SupportedFacetType::SecurityPolicy => self.generate_security_policy_content(params),
384            SupportedFacetType::Scorecard => self.generate_scorecard_content(params),
385            SupportedFacetType::SecurityInsights => self.generate_security_insights_content(params),
386            SupportedFacetType::SAST => self.generate_sast_content(params),
387            _ => todo!("Not implemented yet"),
388        }
389    }
390}
391impl DefaultSourceBundleContentHandler {
392    fn generate_readme_content(
393        &self,
394        params: &SourceBundleFacetCreateParams,
395    ) -> Result<SourceBundleContent, SkootError> {
396        #[derive(Template)]
397        #[template(path = "README.md", escape = "none")]
398        struct ReadmeTemplateParams {
399            project_name: String,
400        }
401
402        let readme_template_params = ReadmeTemplateParams {
403            project_name: params.common.project_name.clone(),
404        };
405
406        let content = readme_template_params.render()?;
407
408        Ok(SourceBundleContent {
409            source_files_content: vec![SourceFileContent {
410                name: "README.md".to_string(),
411                path: "./".to_string(),
412                content,
413            }],
414            facet_type: SupportedFacetType::Readme,
415        })
416    }
417    // TODO: Support more than Apache 2.0
418    fn generate_license_content(
419        &self,
420        params: &SourceBundleFacetCreateParams,
421    ) -> Result<SourceBundleContent, SkootError> {
422        #[derive(Template)]
423        #[template(path = "LICENSE", escape = "none")]
424        struct LicenseTemplateParams {
425            project_name: String,
426            date: i32,
427        }
428
429        let license_template_params = LicenseTemplateParams {
430            project_name: params.common.project_name.clone(),
431            date: chrono::Utc::now().year(),
432        };
433
434        let content = license_template_params.render()?;
435
436        Ok(SourceBundleContent {
437            source_files_content: vec![SourceFileContent {
438                name: "LICENSE".to_string(),
439                path: "./".to_string(),
440                content,
441            }],
442            facet_type: SupportedFacetType::License,
443        })
444    }
445    // TODO: Create actual security policy
446    fn generate_security_policy_content(
447        &self,
448        _params: &SourceBundleFacetCreateParams,
449    ) -> Result<SourceBundleContent, SkootError> {
450        // TODO: Turn this into a real default security policy
451        #[derive(Template)]
452        #[template(path = "SECURITY.prerelease.md", escape = "none")]
453        struct SecurityPolicyTemplateParams {}
454
455        let security_policy_template_params = SecurityPolicyTemplateParams {};
456        let content = security_policy_template_params.render()?;
457
458        Ok(SourceBundleContent {
459            source_files_content: vec![SourceFileContent {
460                name: "SECURITY.md".to_string(),
461                path: "./".to_string(),
462                content,
463            }],
464            facet_type: SupportedFacetType::SecurityPolicy,
465        })
466    }
467
468    fn generate_scorecard_content(
469        &self,
470        _params: &SourceBundleFacetCreateParams,
471    ) -> Result<SourceBundleContent, SkootError> {
472        // TODO: This should serialize to yaml instead of just a file template
473        #[derive(Template)]
474        #[template(path = "scorecard.yml", escape = "none")]
475        struct ScorecardTemplateParams {}
476
477        let scorecard_template_params = ScorecardTemplateParams {};
478        let content = scorecard_template_params.render()?;
479
480        Ok(SourceBundleContent {
481            source_files_content: vec![SourceFileContent {
482                name: "scorecard.yml".to_string(),
483                path: "./.github/workflows".to_string(),
484                content,
485            }],
486            facet_type: SupportedFacetType::Scorecard,
487        })
488    }
489
490    #[allow(clippy::too_many_lines)]
491    fn generate_security_insights_content(
492        &self,
493        params: &SourceBundleFacetCreateParams,
494    ) -> Result<SourceBundleContent, SkootError> {
495        let insights = SecurityInsightsVersion100YamlSchema {
496            contribution_policy: SecurityInsightsVersion100YamlSchemaContributionPolicy {
497                accepts_automated_pull_requests: true,
498                accepts_pull_requests: true,
499                automated_tools_list: None,
500                code_of_conduct: None,
501                contributing_policy: None,
502            },
503            dependencies: Some(SecurityInsightsVersion100YamlSchemaDependencies{
504                dependencies_lifecycle: None,
505                dependencies_lists: vec![
506                    format!("{}/blob/main/go.mod", &params.common.repo.full_url())
507                ],
508                env_dependencies_policy: None,
509                sbom: Some(vec![
510                    SecurityInsightsVersion100YamlSchemaDependenciesSbomItem {
511                        sbom_creation: Some(
512                            SecurityInsightsVersion100YamlSchemaDependenciesSbomItemSbomCreation::from_str("Created by goreleaser")?),
513                        sbom_file: Some(format!("{}/releases/latest/download/main-linux-amd64.spdx.sbom.json", &params.common.repo.full_url())), 
514                        sbom_format: Some("SPDX".to_string()),
515                        sbom_url: Some("https://spdx.github.io/spdx-spec/v2.3/".to_string()), 
516                    },
517                    SecurityInsightsVersion100YamlSchemaDependenciesSbomItem {
518                        sbom_creation: Some(
519                            SecurityInsightsVersion100YamlSchemaDependenciesSbomItemSbomCreation::from_str("Created by goreleaser")?),
520                        sbom_file: Some(format!("{}/releases/latest/download/main-linux-arm.spdx.sbom.json", &params.common.repo.full_url())), 
521                        sbom_format: Some("SPDX".to_string()),
522                        sbom_url: Some("https://spdx.github.io/spdx-spec/v2.3/".to_string()), 
523                    },
524                    SecurityInsightsVersion100YamlSchemaDependenciesSbomItem {
525                        sbom_creation: Some(
526                            SecurityInsightsVersion100YamlSchemaDependenciesSbomItemSbomCreation::from_str("Created by goreleaser")?),
527                        sbom_file: Some(format!("{}/releases/latest/download/main-linux-arm64.spdx.sbom.json", &params.common.repo.full_url())), 
528                        sbom_format: Some("SPDX".to_string()),
529                        sbom_url: Some("https://spdx.github.io/spdx-spec/v2.3/".to_string()), 
530                    },
531                    SecurityInsightsVersion100YamlSchemaDependenciesSbomItem {
532                        sbom_creation: Some(
533                            SecurityInsightsVersion100YamlSchemaDependenciesSbomItemSbomCreation::from_str("Created by goreleaser")?),
534                        sbom_file: Some(format!("{}/releases/latest/download/main-windows-amd64.exe.spdx.sbom.json", &params.common.repo.full_url())), 
535                        sbom_format: Some("SPDX".to_string()),
536                        sbom_url: Some("https://spdx.github.io/spdx-spec/v2.3/".to_string()), 
537                    },
538                    SecurityInsightsVersion100YamlSchemaDependenciesSbomItem {
539                        sbom_creation: Some(
540                            SecurityInsightsVersion100YamlSchemaDependenciesSbomItemSbomCreation::from_str("Created by goreleaser")?),
541                        sbom_file: Some(format!("{}/releases/latest/download/main.spdx.sbom.json", &params.common.repo.full_url())), 
542                        sbom_format: Some("SPDX".to_string()),
543                        sbom_url: Some("https://spdx.github.io/spdx-spec/v2.3/".to_string()), 
544                    },
545                ]),
546                third_party_packages: Some(true),
547            }),
548            distribution_points: Vec::new(),
549            documentation: None,
550            header: SecurityInsightsVersion100YamlSchemaHeader {
551                changelog: None,
552                commit_hash: None,
553                expiration_date: chrono::Utc::now() + chrono::Duration::days(365),
554                last_reviewed: Some(chrono::Utc::now()),
555                last_updated: Some(chrono::Utc::now()),
556                license: Some(format!(
557                    "{}/blob/main/LICENSE",
558                    &params.common.repo.full_url()
559                )),
560                project_release: None,
561                project_url: params.common.repo.full_url(),
562                schema_version: SecurityInsightsVersion100YamlSchemaHeaderSchemaVersion::_100,
563            },
564            project_lifecycle: SecurityInsightsVersion100YamlSchemaProjectLifecycle {
565                bug_fixes_only: false,
566                core_maintainers: None,
567                release_cycle: None,
568                release_process: None,
569                roadmap: None,
570                status: SecurityInsightsVersion100YamlSchemaProjectLifecycleStatus::Active,
571            },
572            // TODO: Since security insights doesn't support SLSA, scorecard, etc. explicitly we might want to add it
573            // to security_artifacts.
574            security_artifacts: None,
575            security_assessments: None,
576            security_contacts: Vec::new(),
577            security_testing: Vec::new(),
578            vulnerability_reporting: SecurityInsightsVersion100YamlSchemaVulnerabilityReporting {
579                accepts_vulnerability_reports: true,
580                bug_bounty_available: None,
581                bug_bounty_url: None,
582                comment: None,
583                email_contact: None,
584                in_scope: None,
585                out_scope: None,
586                pgp_key: None,
587                security_policy: Some(format!("{}/blob/main/SECURITY.md", &params.common.repo.full_url())),
588            },
589        };
590
591        let content = serde_yaml::to_string(&insights)?;
592
593        Ok(SourceBundleContent {
594            source_files_content: vec![SourceFileContent {
595                name: "SECURITY-INSIGHTS.yml".to_string(),
596                path: "./".to_string(),
597                content,
598            }],
599            facet_type: SupportedFacetType::SecurityInsights,
600        })
601    }
602
603    fn generate_sast_content(
604        &self,
605        _params: &SourceBundleFacetCreateParams,
606    ) -> Result<SourceBundleContent, SkootError> {
607        #[derive(Template)]
608        #[template(path = "codeql.yml", escape = "none")]
609        struct SASTTemplateParams {}
610
611        let sast_template_params = SASTTemplateParams {};
612        let content = sast_template_params.render()?;
613
614        Ok(SourceBundleContent {
615            source_files_content: vec![SourceFileContent {
616                name: "codeql.yml".to_string(),
617                path: "./.github/workflows".to_string(),
618                content,
619            }],
620            facet_type: SupportedFacetType::SAST,
621        })
622    }
623}
624
625/// Handles the generation of source files content specific to Go projects hosted on Github.
626/// e.g. Github actions running goreleaser
627struct GoGithubSourceBundleContentHandler {}
628
629impl SourceBundleContentGenerator for GoGithubSourceBundleContentHandler {
630    fn generate_content(
631        &self,
632        params: &SourceBundleFacetCreateParams,
633    ) -> Result<SourceBundleContent, SkootError> {
634        match params.facet_type {
635            SupportedFacetType::Gitignore => self.generate_gitignore_content(params),
636            // TODO: Rename this to something like SecureBuild.
637            // This also does a bunch of other stuff like setting up releases, generating SBOM, etc.
638            // So for now just we just use it instead of creating multiple facets.
639            // The better option is to probably set up some mapping of properties like SLSA, SBOMGenerating, etc.
640            // to a single SecureBuild facet.
641            SupportedFacetType::SLSABuild => self.generate_slsa_build_content(params),
642            SupportedFacetType::DependencyUpdateTool => {
643                self.generate_dependency_update_tool_content(params)
644            }
645            SupportedFacetType::Fuzzing => self.generate_fuzzing_content(params),
646            SupportedFacetType::DefaultSourceCode => {
647                self.generate_default_source_code_content(params)
648            }
649            _ => todo!("Not implemented yet"),
650        }
651    }
652}
653impl GoGithubSourceBundleContentHandler {
654    fn generate_gitignore_content(
655        &self,
656        _params: &SourceBundleFacetCreateParams,
657    ) -> Result<SourceBundleContent, SkootError> {
658        #[derive(Template)]
659        #[template(path = "go.gitignore", escape = "none")]
660        struct GitignoreTemplateParams {}
661
662        let gitignore_template_params = GitignoreTemplateParams {};
663        let content = gitignore_template_params.render()?;
664
665        Ok(SourceBundleContent {
666            source_files_content: vec![SourceFileContent {
667                name: ".gitignore".to_string(),
668                path: "./".to_string(),
669                content,
670            }],
671            facet_type: SupportedFacetType::Gitignore,
672        })
673    }
674    // Note: GoReleaser also does a bunch of other stuff like setting up releases, generating SBOM, etc.
675    // So for now just we just use it instead of creating multiple facets.
676    // Note: Content mostly taken from https://github.com/guacsec/guac/blob/f1703bd4ca3c0ec0fa55c5a3401d50578fb1680e/.github/workflows/release.yaml
677    fn generate_slsa_build_content(
678        &self,
679        params: &SourceBundleFacetCreateParams,
680    ) -> Result<SourceBundleContent, SkootError> {
681        // TODO: This should really be a struct that serializes to yaml instead of just a file template
682        #[derive(Template)]
683        #[template(path = "go.releases.yml", escape = "none")]
684        struct ReleaseTemplateParams {}
685
686        #[derive(Template)]
687        #[template(path = "Dockerfile.goreleaser", escape = "none")]
688        struct DockerfileTemplateParams {
689            project_name: String,
690        }
691
692        #[derive(Template)]
693        #[template(path = "goreleaser.yml", escape = "none")]
694        struct GoReleaserTemplateParams {
695            project_name: String,
696            module_name: String,
697        }
698
699        #[allow(clippy::match_wildcard_for_single_variants)]
700        let module = match &params.common.ecosystem {
701            InitializedEcosystem::Go(go) => go.module(),
702            _ => unreachable!("Ecosystem should be Go"),
703        };
704
705        let slsa_build_template_params = ReleaseTemplateParams {};
706        let dockerfile_template_params = DockerfileTemplateParams {
707            project_name: params.common.project_name.clone(),
708        };
709        let goreleaser_template_params = GoReleaserTemplateParams {
710            project_name: params.common.project_name.clone(),
711            module_name: module,
712        };
713
714        Ok(SourceBundleContent {
715            source_files_content: vec![
716                SourceFileContent {
717                    name: "releases.yml".to_string(),
718                    path: ".github/workflows/".to_string(),
719                    content: slsa_build_template_params.render()?,
720                },
721                SourceFileContent {
722                    name: "Dockerfile.goreleaser".to_string(),
723                    path: "./".to_string(),
724                    content: dockerfile_template_params.render()?,
725                },
726                SourceFileContent {
727                    name: ".goreleaser.yml".to_string(),
728                    path: "./".to_string(),
729                    content: goreleaser_template_params.render()?,
730                },
731            ],
732            facet_type: SupportedFacetType::SLSABuild,
733        })
734    }
735
736    fn generate_dependency_update_tool_content(
737        &self,
738        _params: &SourceBundleFacetCreateParams,
739    ) -> Result<SourceBundleContent, SkootError> {
740        #[derive(Template)]
741        #[template(path = "dependabot.yml", escape = "none")]
742        struct DependabotTemplateParams {
743            ecosystem: String,
744        }
745
746        let dependabot_template_params = DependabotTemplateParams {
747            ecosystem: "gomod".to_string(),
748        };
749        let content = dependabot_template_params.render()?;
750
751        Ok(SourceBundleContent {
752            source_files_content: vec![SourceFileContent {
753                name: "dependabot.yml".to_string(),
754                path: ".github/".to_string(),
755                content,
756            }],
757            facet_type: SupportedFacetType::DependencyUpdateTool,
758        })
759    }
760
761    fn generate_fuzzing_content(
762        &self,
763        params: &SourceBundleFacetCreateParams,
764    ) -> Result<SourceBundleContent, SkootError> {
765        #[derive(Template)]
766        #[template(path = "cifuzz.yml", escape = "none")]
767        struct FuzzingTemplateParams {
768            project_name: String,
769            language: String,
770        }
771
772        let fuzzing_template_params = FuzzingTemplateParams {
773            project_name: params.common.project_name.clone(),
774            language: "go".to_string(),
775        };
776        let content = fuzzing_template_params.render()?;
777
778        Ok(SourceBundleContent {
779            source_files_content: vec![SourceFileContent {
780                name: "cifuzz.yml".to_string(),
781                path: ".github/workflows/".to_string(),
782                content,
783            }],
784            facet_type: SupportedFacetType::Fuzzing,
785        })
786    }
787
788    fn generate_default_source_code_content(
789        &self,
790        _params: &SourceBundleFacetCreateParams,
791    ) -> Result<SourceBundleContent, SkootError> {
792        #[derive(Template)]
793        #[template(path = "main.go.tmpl", escape = "none")]
794        struct DefaultSourceCodeTemplateParams {}
795
796        let default_source_code_template_params = DefaultSourceCodeTemplateParams {};
797        let content = default_source_code_template_params.render()?;
798
799        Ok(SourceBundleContent {
800            source_files_content: vec![SourceFileContent {
801                name: "main.go".to_string(),
802                path: "./".to_string(),
803                content,
804            }],
805            facet_type: SupportedFacetType::DefaultSourceCode,
806        })
807    }
808}
809
810/// The `FacetSetParamsGenerator` struct represents a service for generating params for a set of facets.
811/// This includes things like generating default params for source bundles and API bundles.
812pub struct FacetSetParamsGenerator {}
813
814impl FacetSetParamsGenerator {
815    /// Generates the default set of facet params for a project.
816    /// This includes things like generating default source bundle and API bundle facet params.
817    ///
818    /// # Errors
819    ///
820    /// Returns an error if any of the facet set params can't be generated.
821    pub fn generate_default(
822        &self,
823        common_params: &CommonFacetCreateParams,
824    ) -> Result<FacetSetCreateParams, SkootError> {
825        let source_bundle_params =
826            self.generate_default_source_bundle_facet_params(common_params)?;
827        let api_bundle_params = self.generate_default_api_bundle(common_params)?;
828        let total_params = FacetSetCreateParams {
829            facets_params: [
830                source_bundle_params.facets_params,
831                api_bundle_params.facets_params,
832            ]
833            .concat(),
834        };
835
836        Ok(total_params)
837    }
838
839    /// Generates the default set of API bundle facet params for a project.
840    ///
841    /// # Errors
842    ///
843    /// Returns an error if the default set of API bundle facets can't be generated.
844    pub fn generate_default_api_bundle(
845        &self,
846        common_params: &CommonFacetCreateParams,
847    ) -> Result<FacetSetCreateParams, SkootError> {
848        use SupportedFacetType::{BranchProtection, VulnerabilityReporting};
849        let supported_facets = [
850            //CodeReview,
851            BranchProtection,
852            VulnerabilityReporting,
853        ];
854        let facets_params = supported_facets
855            .iter()
856            .map(|facet_type| {
857                FacetCreateParams::APIBundle(APIBundleFacetParams {
858                    common: common_params.clone(),
859                    facet_type: facet_type.clone(),
860                })
861            })
862            .collect::<Vec<FacetCreateParams>>();
863
864        Ok(FacetSetCreateParams { facets_params })
865    }
866
867    // TODO: Come up with a better solution than hard coding the default facets
868    /// Generates the default set of source bundle facet params for a project.
869    ///
870    /// # Errors
871    ///
872    /// Returns an error if the default set of source bundle facets can't be generated.
873    pub fn generate_default_source_bundle_facet_params(
874        &self,
875        common_params: &CommonFacetCreateParams,
876    ) -> Result<FacetSetCreateParams, SkootError> {
877        use SupportedFacetType::{
878            DefaultSourceCode, DependencyUpdateTool, Gitignore, License, Readme, SLSABuild,
879            Scorecard, SecurityInsights, SecurityPolicy, SAST,
880        };
881        let supported_facets = [
882            FacetTypeLabels {
883                supported_facet_type: Readme,
884                labels: vec![],
885            },
886            FacetTypeLabels {
887                supported_facet_type: License,
888                labels: vec![],
889            },
890            FacetTypeLabels {
891                supported_facet_type: Gitignore,
892                labels: vec![],
893            },
894            FacetTypeLabels {
895                supported_facet_type: SecurityPolicy,
896                labels: vec![],
897            },
898            FacetTypeLabels {
899                supported_facet_type: SecurityInsights,
900                labels: vec![],
901            },
902            FacetTypeLabels {
903                supported_facet_type: SLSABuild,
904                labels: vec![Label::SLSABuildLevel3, Label::S2C2FAUD1],
905            },
906            // SBOMGenerator, // Handled by the SLSABuild facet
907            // StaticCodeAnalysis,
908            FacetTypeLabels {
909                supported_facet_type: DependencyUpdateTool,
910                labels: vec![Label::S2C2FUPD2],
911            },
912            // TODO: Fuzzing right now requires a bunch of resources that are unavailable to most projects without
913            // some sort of manual intervention. This is disabled until some option becomes available.
914            // Fuzzing,
915            FacetTypeLabels {
916                supported_facet_type: Scorecard,
917                labels: vec![],
918            },
919            // PublishPackages,
920            // PinnedDependencies,
921            FacetTypeLabels {
922                supported_facet_type: SAST,
923                labels: vec![Label::S2C2FSCA1],
924            },
925            // VulnerabilityScanner,
926            // GUACForwardingConfig,
927            // These are at the end to allow Skootrs to push initial commits without needing
928            // code review or branches.
929            // CodeReview, // TODO: Implement this
930            //BranchProtection, //TODO: Implement this
931            FacetTypeLabels {
932                supported_facet_type: DefaultSourceCode,
933                labels: vec![],
934            },
935        ];
936        let facets_params = supported_facets
937            .iter()
938            .map(|facet_type_labels| {
939                FacetCreateParams::SourceBundle(SourceBundleFacetCreateParams {
940                    common: common_params.clone(),
941                    facet_type: facet_type_labels.supported_facet_type.clone(),
942                    labels: facet_type_labels.labels.clone(),
943                })
944            })
945            .collect::<Vec<FacetCreateParams>>();
946
947        Ok(FacetSetCreateParams { facets_params })
948    }
949}
950
951struct FacetTypeLabels {
952    supported_facet_type: SupportedFacetType,
953    labels: Vec<Label>,
954}