1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
//! Secrets Manager: listing an account's secrets and reading a value
//! (redacted by default at the render layer, not here).
use super::*;
/// One row in the `:secrets` listing — metadata only, no values.
/// Value retrieval happens via `fetch_secret_value` on demand
/// because every `GetSecretValue` call is a separate audit-loggable
/// AWS event the operator should opt into explicitly.
#[derive(Clone, Debug)]
pub struct SecretSummary {
pub name: String,
pub arn: String,
pub description: Option<String>,
pub last_changed: Option<DateTime<Utc>>,
pub last_rotated: Option<DateTime<Utc>>,
pub kms_key_id: Option<String>,
}
impl AwsClient {
/// List Secrets Manager secrets in the active region.
/// `name_filter` is an optional substring match against the
/// secret name (case-sensitive — Secrets Manager's
/// `Filters.Key=name` does prefix matching only, so we
/// post-filter for substring instead).
///
/// Paginates internally. Returns the metadata rows; no
/// secret *values* are fetched here — see [`AwsClient::fetch_secret_value`].
pub async fn list_secrets(&self, name_filter: Option<&str>) -> Result<Vec<SecretSummary>> {
let this = self;
let page =
super::paginate_capped("ListSecrets", super::SCAN_PAGES, move |token| async move {
let mut req = this.secrets().list_secrets().max_results(100);
if let Some(t) = token {
req = req.next_token(t);
}
let resp = req.send().await.wrap_err("ListSecrets failed")?;
Ok((resp.secret_list.unwrap_or_default(), resp.next_token))
})
.await?;
// Completeness is only load-bearing when the caller asked about
// a *specific* secret: then a cut-short scan reads as "it
// doesn't exist". An unfiltered browse is served fine by a
// partial list, and refusing it outright would be worse than
// showing the first several thousand.
let raw = if name_filter.is_some() {
page.complete("ListSecrets")?
} else {
page.items()
};
let mut out: Vec<SecretSummary> = raw
.into_iter()
.filter_map(|s| {
let name = s.name.filter(|n| !n.is_empty())?;
if let Some(needle) = name_filter {
if !name.contains(needle) {
return None;
}
}
Some(SecretSummary {
name,
arn: s.arn.unwrap_or_default(),
description: s.description.filter(|d| !d.is_empty()),
last_changed: s
.last_changed_date
.and_then(|d| DateTime::from_timestamp(d.secs(), d.subsec_nanos())),
last_rotated: s
.last_rotated_date
.and_then(|d| DateTime::from_timestamp(d.secs(), d.subsec_nanos())),
kms_key_id: s.kms_key_id.filter(|k| !k.is_empty()),
})
})
.collect();
// Stable order — most-recently-changed first so freshly
// rotated secrets float to the top of the picker.
out.sort_by_key(|r| std::cmp::Reverse(r.last_changed));
Ok(out)
}
/// `GetSecretValue` for one secret. Returns the value verbatim —
/// caller decides whether to display, redact, or yank.
/// Audit-loggable on the AWS side (CloudTrail logs every
/// GetSecretValue); ebman additionally writes its own audit
/// line via the caller path.
pub async fn fetch_secret_value(&self, secret_id: &str) -> Result<String> {
let resp = self
.secrets()
.get_secret_value()
.secret_id(secret_id)
.send()
.await
.wrap_err("GetSecretValue failed")?;
// Secrets Manager returns either SecretString (UTF-8 text,
// including JSON for k/v secrets) or SecretBinary (base64
// blob). Prefer the string; fall back to noting the binary
// length so the operator doesn't try to inspect.
if let Some(s) = resp.secret_string {
return Ok(s);
}
if let Some(b) = resp.secret_binary {
return Ok(format!("(binary, {} bytes — not shown)", b.as_ref().len()));
}
Ok(String::new())
}
}