Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a07fe6a

Browse files
authored
impostor-commit: remove autofix (#2054)
* impostor-commit: remove autofix The more I think about this, the more I'm convinced it doesn't make sense to have an autofix here: a true-positive impostor commit is a very strong signal of total compromise, and silently moving the user to a real ref might give them the false impression that everything is fixed when really they need to perform manual, in-depth triage. * Remove more dead code * Update snapshot
1 parent 539ad52 commit a07fe6a

4 files changed

Lines changed: 14 additions & 246 deletions

File tree

crates/zizmor/src/audit/impostor_commit.rs

Lines changed: 5 additions & 238 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,12 @@ use super::{Audit, AuditLoadError, Job, audit_meta};
1515
use 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-
3524
pub const IMPOSTOR_ANNOTATION: &str = "uses a commit that doesn't belong to the specified org/repo";
3625

3726
pub(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-
}

crates/zizmor/src/models/version.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ static_regex!(
2828
#[derive(Eq)]
2929
pub(crate) struct Version<'a> {
3030
/// The raw version, exactly as it appears in its source.
31+
#[allow(dead_code)]
3132
raw: &'a str,
3233
major: u64,
3334
minor: u64,
@@ -79,11 +80,6 @@ impl<'a> Version<'a> {
7980
patch,
8081
})
8182
}
82-
83-
/// Return the raw version string, exactly as it was parsed.
84-
pub(crate) fn raw(&self) -> &'a str {
85-
self.raw
86-
}
8783
}
8884

8985
impl Ord for Version<'_> {
@@ -136,7 +132,7 @@ mod tests {
136132
assert_eq!(version.major, exp_major);
137133
assert_eq!(version.minor, exp_minor);
138134
assert_eq!(version.patch, exp_patch);
139-
assert_eq!(version.raw(), input);
135+
assert_eq!(version.raw, input);
140136
}
141137
}
142138

crates/zizmor/tests/integration/audit/impostor_commit.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@ fn test_regular_persona() -> anyhow::Result<()> {
2121
| |____________________________________- this step
2222
|
2323
= note: audit confidence → High
24-
= note: this finding has an auto-fix
2524
26-
4 findings (3 suppressed, 1 unsafe fixes): 0 informational, 0 low, 0 medium, 1 high
25+
4 findings (3 suppressed): 0 informational, 0 low, 0 medium, 1 high
2726
"
2827
);
2928

docs/release-notes.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ of `zizmor`.
2929

3030
* Fixed a bug where [ref-version-mismatch] would fail to fully match some version comments (#2040)
3131

32+
### Changes ⚠️
33+
34+
* The [impostor-commit] audit no longer suggests auto-fixes,
35+
to avoid incorrectly minimizing the amount of manual remediation
36+
work needed (#2054)
37+
3238
## 1.25.2
3339

3440
### Bug Fixes 🐛

0 commit comments

Comments
 (0)