@@ -15,23 +15,12 @@ use super::{Audit, AuditLoadError, Job, audit_meta};
1515use crate :: {
1616 audit:: AuditError ,
1717 config:: Config ,
18- finding:: {
19- Confidence , Finding , Fix , FixDisposition , Severity ,
20- location:: { Locatable as _, Routable } ,
21- } ,
18+ finding:: { Confidence , Finding , Severity , location:: Locatable as _} ,
2219 github:: { self , ComparisonStatus } ,
23- models:: {
24- StepCommon ,
25- uses:: RepositoryUsesExt as _,
26- version:: Version ,
27- workflow:: { ReusableWorkflowCallJob , Workflow } ,
28- } ,
29- registry:: input:: InputKey ,
20+ models:: { StepCommon , uses:: RepositoryUsesExt as _, workflow:: Workflow } ,
3021 state:: AuditState ,
3122} ;
3223
33- use yamlpatch:: { Op , Patch } ;
34-
3524pub const IMPOSTOR_ANNOTATION : & str = "uses a commit that doesn't belong to the specified org/repo" ;
3625
3726pub ( crate ) struct ImpostorCommit {
@@ -278,99 +267,6 @@ impl ImpostorCommit {
278267 _ => Ok ( true ) ,
279268 }
280269 }
281-
282- /// Return the highest semantically versioned tag in the repository.
283- async fn get_highest_tag ( & self , uses : & RepositoryUses ) -> Result < Option < String > , AuditError > {
284- let tags = self
285- . client
286- . list_tags ( uses. owner ( ) , uses. repo ( ) )
287- . await
288- . map_err ( Self :: err) ?;
289-
290- // Filter tags down to those that can be parsed as semantic versions,
291- // get the highest one, and return its original string representation.
292- let highest_tag = tags
293- . iter ( )
294- . filter_map ( |tag| Version :: parse ( & tag. name ) . ok ( ) )
295- . max ( )
296- . map ( |vers| vers. raw ( ) . to_string ( ) ) ;
297-
298- Ok ( highest_tag)
299- }
300-
301- /// Create a fix for an impostor commit by replacing it with the latest tag
302- async fn create_impostor_fix < ' doc , T > (
303- & self ,
304- uses : & RepositoryUses ,
305- step : & T ,
306- ) -> Option < Fix < ' doc > >
307- where
308- T : StepCommon < ' doc > + for < ' a > Routable < ' a , ' doc > ,
309- {
310- self . create_fix_for_location ( uses, step. location ( ) . key , step. route ( ) . with_key ( "uses" ) )
311- . await
312- }
313-
314- /// Create a fix for a reusable workflow job
315- async fn create_reusable_fix < ' doc > (
316- & self ,
317- uses : & RepositoryUses ,
318- job : & ReusableWorkflowCallJob < ' doc > ,
319- ) -> Option < Fix < ' doc > > {
320- self . create_fix_for_location (
321- uses,
322- job. location ( ) . key ,
323- job. location ( ) . route . with_key ( "uses" ) ,
324- )
325- . await
326- }
327-
328- /// Create a fix for the given location parameters
329- async fn create_fix_for_location < ' doc > (
330- & self ,
331- uses : & RepositoryUses ,
332- key : & ' doc InputKey ,
333- route : yamlpath:: Route < ' doc > ,
334- ) -> Option < Fix < ' doc > > {
335- // Get the latest tag for this repository
336- let latest_tag = match self . get_highest_tag ( uses) . await {
337- Ok ( Some ( tag) ) => tag,
338- Ok ( None ) => {
339- tracing:: warn!(
340- "No tags found for {}/{}, cannot create fix" ,
341- uses. owner( ) ,
342- uses. repo( )
343- ) ;
344- return None ;
345- }
346- Err ( e) => {
347- tracing:: error!(
348- "Failed to get latest tag for {}/{}: {}" ,
349- uses. owner( ) ,
350- uses. repo( ) ,
351- e
352- ) ;
353- return None ;
354- }
355- } ;
356-
357- // Build the new uses string with the latest tag
358- let mut uses_slug = format ! ( "{}/{}" , uses. owner( ) , uses. repo( ) ) ;
359- if let Some ( subpath) = & uses. subpath ( ) {
360- uses_slug. push_str ( & format ! ( "/{subpath}" ) ) ;
361- }
362- let fixed_uses = format ! ( "{uses_slug}@{latest_tag}" ) ;
363-
364- Some ( Fix {
365- title : format ! ( "pin to latest tag {latest_tag}" ) ,
366- key,
367- disposition : FixDisposition :: Unsafe ,
368- patches : vec ! [ Patch {
369- route,
370- operation: Op :: Replace ( fixed_uses. into( ) ) ,
371- } ] ,
372- } )
373- }
374270}
375271
376272#[ async_trait:: async_trait]
@@ -405,7 +301,7 @@ impl Audit for ImpostorCommit {
405301 } ;
406302
407303 if self . impostor ( uses) . await ? {
408- let mut finding_builder = Self :: finding ( )
304+ let finding_builder = Self :: finding ( )
409305 . severity ( Severity :: High )
410306 . confidence ( Confidence :: High )
411307 . add_location ( step. location_with_grip ( ) )
@@ -417,10 +313,6 @@ impl Audit for ImpostorCommit {
417313 . annotated ( IMPOSTOR_ANNOTATION ) ,
418314 ) ;
419315
420- if let Some ( fix) = self . create_impostor_fix ( uses, & step) . await {
421- finding_builder = finding_builder. fix ( fix) ;
422- }
423-
424316 findings. push ( finding_builder. build ( workflow) . map_err ( Self :: err) ?) ;
425317 }
426318 }
@@ -433,7 +325,7 @@ impl Audit for ImpostorCommit {
433325 } ;
434326
435327 if self . impostor ( uses) . await ? {
436- let mut finding_builder = Self :: finding ( )
328+ let finding_builder = Self :: finding ( )
437329 . severity ( Severity :: High )
438330 . confidence ( Confidence :: High )
439331 . add_location ( reusable. location_with_grip ( ) )
@@ -446,10 +338,6 @@ impl Audit for ImpostorCommit {
446338 . annotated ( IMPOSTOR_ANNOTATION ) ,
447339 ) ;
448340
449- if let Some ( fix) = self . create_reusable_fix ( uses, & reusable) . await {
450- finding_builder = finding_builder. fix ( fix) ;
451- }
452-
453341 findings. push ( finding_builder. build ( workflow) . map_err ( Self :: err) ?) ;
454342 }
455343 }
@@ -470,7 +358,7 @@ impl Audit for ImpostorCommit {
470358 } ;
471359
472360 if self . impostor ( uses) . await ? {
473- let mut finding_builder = Self :: finding ( )
361+ let finding_builder = Self :: finding ( )
474362 . severity ( Severity :: High )
475363 . confidence ( Confidence :: High )
476364 . add_location ( step. location_with_grip ( ) )
@@ -482,130 +370,9 @@ impl Audit for ImpostorCommit {
482370 . annotated ( IMPOSTOR_ANNOTATION ) ,
483371 ) ;
484372
485- if let Some ( fix) = self . create_impostor_fix ( uses, step) . await {
486- finding_builder = finding_builder. fix ( fix) ;
487- }
488-
489373 findings. push ( finding_builder. build ( step) . map_err ( Self :: err) ?) ;
490374 }
491375
492376 Ok ( findings)
493377 }
494378}
495-
496- #[ cfg( test) ]
497- mod tests {
498-
499- #[ cfg( feature = "gh-token-tests" ) ]
500- #[ tokio:: test]
501- async fn test_impostor_commit_fix_snapshot ( ) {
502- use insta:: assert_snapshot;
503-
504- use crate :: models:: AsDocument as _;
505-
506- use super :: * ;
507- use crate :: { models:: workflow:: Workflow , registry:: input:: InputKey } ;
508-
509- // Test with a workflow that uses a commit hash that doesn't exist in the target repository
510- // We'll use actions/hello-world-javascript-action with a commit from actions/checkout
511- // This creates an impostor scenario: valid commit, wrong repository
512- let workflow_content = r#"
513- name: Test Impostor Commit Fix
514- on: push
515- jobs:
516- test:
517- runs-on: ubuntu-latest
518- steps:
519- - uses: actions/hello-world-javascript-action@692973e3d937129bcbf40652eb9f2f61becf3332 # This is a commit from actions/checkout, not hello-world
520- "# ;
521-
522- let key = InputKey :: local ( "dummy" . into ( ) , "test.yml" , None :: < & str > ) ;
523- let workflow = Workflow :: from_string ( workflow_content. to_string ( ) , key) . unwrap ( ) ;
524-
525- let state = crate :: state:: AuditState {
526- no_online_audits : false ,
527- gh_client : Some (
528- crate :: github:: Client :: new (
529- & crate :: github:: GitHubHost :: Standard ( "github.com" . to_string ( ) ) ,
530- & crate :: github:: GitHubToken :: new ( & std:: env:: var ( "GH_TOKEN" ) . unwrap ( ) ) . unwrap ( ) ,
531- "/tmp" . into ( ) ,
532- )
533- . unwrap ( ) ,
534- ) ,
535- } ;
536-
537- let audit = ImpostorCommit :: new ( & state) . unwrap ( ) ;
538- let input = workflow. into ( ) ;
539- let findings = audit
540- . audit ( "impostor-commit" , & input, & Config :: default ( ) )
541- . await
542- . unwrap ( ) ;
543-
544- // If we detect an impostor commit, there should be a fix available
545- if !findings. is_empty ( ) {
546- assert ! (
547- !findings[ 0 ] . fixes. is_empty( ) ,
548- "Expected fix for impostor commit"
549- ) ;
550-
551- // Apply the fix and snapshot test the result
552- let new_doc = findings[ 0 ] . fixes [ 0 ] . apply ( input. as_document ( ) ) . unwrap ( ) ;
553- assert_snapshot ! ( new_doc. source( ) , @"
554-
555- name: Test Impostor Commit Fix
556- on: push
557- jobs:
558- test:
559- runs-on: ubuntu-latest
560- steps:
561- - uses: actions/[email protected] # This is a commit from actions/checkout, not hello-world 562- " ) ;
563- }
564- }
565-
566- #[ cfg( feature = "gh-token-tests" ) ]
567- #[ tokio:: test]
568- async fn test_no_impostor_with_valid_tag ( ) {
569- use super :: * ;
570- use crate :: { models:: workflow:: Workflow , registry:: input:: InputKey } ;
571-
572- // Test with a valid tag to ensure we don't get false positives
573- let workflow_content = r#"
574- name: Test Valid Tag
575- on: push
576- jobs:
577- test:
578- runs-on: ubuntu-latest
579- steps:
580- - uses: actions/checkout@v4
581- "# ;
582-
583- let key = InputKey :: local ( "dummy" . into ( ) , "test.yml" , None :: < & str > ) ;
584- let workflow = Workflow :: from_string ( workflow_content. to_string ( ) , key) . unwrap ( ) ;
585-
586- let state = crate :: state:: AuditState {
587- no_online_audits : false ,
588- gh_client : Some (
589- crate :: github:: Client :: new (
590- & crate :: github:: GitHubHost :: Standard ( "github.com" . to_string ( ) ) ,
591- & crate :: github:: GitHubToken :: new ( & std:: env:: var ( "GH_TOKEN" ) . unwrap ( ) ) . unwrap ( ) ,
592- "/tmp" . into ( ) ,
593- )
594- . unwrap ( ) ,
595- ) ,
596- } ;
597-
598- let audit = ImpostorCommit :: new ( & state) . unwrap ( ) ;
599- let input = workflow. into ( ) ;
600- let findings = audit
601- . audit ( "impostor-commit" , & input, & Config :: default ( ) )
602- . await
603- . unwrap ( ) ;
604-
605- // With a valid tag, we should not find any impostor commits
606- assert ! (
607- findings. is_empty( ) ,
608- "Valid tags should not be flagged as impostor commits"
609- ) ;
610- }
611- }
0 commit comments