finalized new detections; removed wildcard filter and supporting code

This commit is contained in:
epi
2023-02-26 06:39:03 -06:00
parent 655364d9bd
commit ad38b56473
8 changed files with 111 additions and 516 deletions

View File

@@ -10,7 +10,7 @@ use crate::{
use super::{
FeroxFilter, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter,
WildcardFilter, WordsFilter,
WordsFilter,
};
/// Container around a collection of `FeroxFilters`s
@@ -76,13 +76,7 @@ impl FeroxFilters {
for filter in filters.iter() {
// wildcard.should_filter goes here
if filter.should_filter_response(response) {
log::warn!("filtering response due to: {:?}", filter);
log::warn!("filtering response due to: {:?}", filters);
if filter.as_any().downcast_ref::<WildcardFilter>().is_some() {
tx_stats
.send(AddToUsizeField(WildcardsFiltered, 1))
.unwrap_or_default();
}
log::debug!("filtering response due to: {:?}", filter);
return true;
}
}
@@ -116,10 +110,6 @@ impl Serialize for FeroxFilters {
filter.as_any().downcast_ref::<SimilarityFilter>()
{
seq.serialize_element(similarity_filter).unwrap_or_default();
} else if let Some(wildcard_filter) =
filter.as_any().downcast_ref::<WildcardFilter>()
{
seq.serialize_element(wildcard_filter).unwrap_or_default();
}
}
seq.end()

View File

@@ -15,7 +15,6 @@ pub use self::similarity::{SimilarityFilter, SIM_HASHER};
pub use self::size::SizeFilter;
pub use self::status_code::StatusCodeFilter;
pub(crate) use self::utils::{create_similarity_filter, filter_lookup};
pub use self::wildcard::WildcardFilter;
pub use self::words::WordsFilter;
mod wildcard;

View File

@@ -1,29 +1,7 @@
use super::*;
use ::fuzzyhash::FuzzyHash;
use ::regex::Regex;
use super::similarity::HashValueType;
#[test]
/// simply test the default values for wildcardfilter, expect 0, 0
fn wildcard_filter_default() {
let wcf = WildcardFilter::default();
assert_eq!(wcf.size, u64::MAX);
assert_eq!(wcf.dynamic, u64::MAX);
}
#[test]
/// just a simple test to increase code coverage by hitting as_any and the inner value
fn wildcard_filter_as_any() {
let filter = WildcardFilter::default();
let filter2 = WildcardFilter::default();
assert!(filter.box_eq(filter2.as_any()));
assert_eq!(
*filter.as_any().downcast_ref::<WildcardFilter>().unwrap(),
filter
);
}
#[test]
/// just a simple test to increase code coverage by hitting as_any and the inner value
@@ -109,59 +87,6 @@ fn regex_filter_as_any() {
);
}
#[test]
/// test should_filter on WilcardFilter where static logic matches
fn wildcard_should_filter_when_static_wildcard_found() {
let mut resp = FeroxResponse::default();
resp.set_wildcard(true);
resp.set_url("http://localhost");
resp.set_text(
"pellentesque diam volutpat commodo sed egestas egestas fringilla phasellus faucibus",
);
let filter = WildcardFilter {
size: 83,
dynamic: 0,
dont_filter: false,
method: "GET".to_owned(),
};
assert!(filter.should_filter_response(&resp));
}
#[test]
/// test should_filter on WilcardFilter where static logic matches but response length is 0
fn wildcard_should_filter_when_static_wildcard_len_is_zero() {
let mut resp = FeroxResponse::default();
resp.set_wildcard(true);
resp.set_url("http://localhost");
// default WildcardFilter is used in the code that executes when response.content_length() == 0
let filter = WildcardFilter::new(false);
assert!(filter.should_filter_response(&resp));
}
#[test]
/// test should_filter on WilcardFilter where dynamic logic matches
fn wildcard_should_filter_when_dynamic_wildcard_found() {
let mut resp = FeroxResponse::default();
resp.set_wildcard(true);
resp.set_url("http://localhost/stuff");
resp.set_text("pellentesque diam volutpat commodo sed egestas egestas fringilla");
let filter = WildcardFilter {
size: 0,
dynamic: 59, // content-length - 5 (len('stuff'))
dont_filter: false,
method: "GET".to_owned(),
};
println!("resp: {resp:?}: filter: {filter:?}");
assert!(filter.should_filter_response(&resp));
}
#[test]
/// test should_filter on RegexFilter where regex matches body
fn regexfilter_should_filter_when_regex_matches_on_response_body() {

View File

@@ -1,118 +0,0 @@
use super::*;
use crate::{url::FeroxUrl, DEFAULT_METHOD};
/// Data holder for two pieces of data needed when auto-filtering out wildcard responses
///
/// `dynamic` is the size of the response that will later be combined with the length
/// of the path of the url requested and used to determine interesting pages from custom
/// 404s where the requested url is reflected back in the response
///
/// `size` is size of the response that should be included with filters passed via runtime
/// configuration and any static wildcard lengths.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WildcardFilter {
/// size of the response that will later be combined with the length of the path of the url
/// requested
pub dynamic: u64,
/// size of the response that should be included with filters passed via runtime configuration
pub size: u64,
/// method used in request that should be included with filters passed via runtime configuration
pub method: String,
/// whether or not the user passed -D on the command line
pub(super) dont_filter: bool,
}
/// implementation of WildcardFilter
impl WildcardFilter {
/// given a boolean representing whether -D was used or not, create a new WildcardFilter
pub fn new(dont_filter: bool) -> Self {
Self {
dont_filter,
..Default::default()
}
}
}
/// implement default that populates both values with u64::MAX
impl Default for WildcardFilter {
/// populate both values with u64::MAX
fn default() -> Self {
Self {
dont_filter: false,
size: u64::MAX,
method: DEFAULT_METHOD.to_owned(),
dynamic: u64::MAX,
}
}
}
/// implementation of FeroxFilter for WildcardFilter
impl FeroxFilter for WildcardFilter {
/// Examine size, dynamic, and content_len to determine whether or not the response received
/// is a wildcard response and therefore should be filtered out
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
log::trace!("enter: should_filter_response({:?} {})", self, response);
// quick return if dont_filter is set
if self.dont_filter {
// --dont-filter applies specifically to wildcard filters, it is not a 100% catch all
// for not filtering anything. As such, it should live in the implementation of
// a wildcard filter
return false;
}
if self.size != u64::MAX
&& self.size == response.content_length()
&& self.method == response.method().as_str()
{
// static wildcard size found during testing
// size isn't default, size equals response length, and auto-filter is on
log::debug!("static wildcard: filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
}
if self.size == u64::MAX
&& response.content_length() == 0
&& self.method == response.method().as_str()
{
// static wildcard size found during testing
// but response length was zero; pointed out by @Tib3rius
log::debug!("static wildcard: filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
}
if self.dynamic != u64::MAX {
// dynamic wildcard offset found during testing
// I'm about to manually split this url path instead of using reqwest::Url's
// builtin parsing. The reason is that they call .split() on the url path
// except that I don't want an empty string taking up the last index in the
// event that the url ends with a forward slash. It's ugly enough to be split
// into its own function for readability.
let url_len = FeroxUrl::path_length_of_url(response.url());
if url_len + self.dynamic == response.content_length() {
log::debug!("dynamic wildcard: filtered out {}", response.url());
log::trace!("exit: should_filter_response -> true");
return true;
}
}
log::trace!("exit: should_filter_response -> false");
false
}
/// Compare one WildcardFilter to another
fn box_eq(&self, other: &dyn Any) -> bool {
other.downcast_ref::<Self>().map_or(false, |a| self == a)
}
/// Return self as Any for dynamic dispatch purposes
fn as_any(&self) -> &dyn Any {
self
}
}

View File

@@ -11,7 +11,7 @@ use crate::nlp::preprocess;
use crate::{
config::OutputLevel,
event_handlers::{Command, Handles},
filters::{LinesFilter, SizeFilter, WildcardFilter, WordsFilter},
filters::{LinesFilter, SizeFilter, WordsFilter},
progress::PROGRESS_PRINTER,
response::FeroxResponse,
skip_fail,
@@ -23,23 +23,6 @@ use crate::{
/// length of a standard UUID, used when determining wildcard responses
const UUID_LENGTH: u64 = 32;
/// wrapper around ugly string formatting
macro_rules! format_template {
($template:expr, $method:expr, $length:expr) => {
format!(
$template,
status_colorizer("WLD"),
$method,
"-",
"-",
"-",
style("auto-filtering").yellow(),
style($length).cyan(),
style("--dont-filter").yellow()
)
};
}
/// enum representing the different servers that `parse_html` can detect when directory listing is
/// enabled
#[derive(Copy, Debug, Clone)]
@@ -76,26 +59,6 @@ pub struct HeuristicTests {
handles: Arc<Handles>,
}
/// simple way to pass around a wildcard filter to the internal helper below
/// in a way that quickly tells us if it's static or dynamic
enum WildcardType<'a> {
Static(&'a WildcardFilter),
Dynamic(&'a WildcardFilter),
}
/// internal helper to stop repeating the same pattern
fn print_dont_filter_message(wildcard_type: WildcardType, output_level: OutputLevel) {
if matches!(output_level, OutputLevel::Default | OutputLevel::Quiet) {
let msg = match wildcard_type {
WildcardType::Static(wc) => {
format_template!("{} {:>8} {:>9} {:>9} {:>9} Wildcard response is static; {} {} responses; toggle this behavior by using {}\n", wc.method.as_str(), wc.size)
}
WildcardType::Dynamic(wc) => format_template!("{} {:>8} {:>9} {:>9} {:>9} Wildcard response is dynamic; {} ({} + url length) responses; toggle this behavior by using {}\n", wc.method.as_str(), wc.dynamic),
};
ferox_print(&msg, &PROGRESS_PRINTER);
}
}
/// HeuristicTests implementation
impl HeuristicTests {
/// create a new HeuristicTests struct
@@ -122,167 +85,6 @@ impl HeuristicTests {
unique_id
}
/// wrapper for sending a filter to the filters event handler
fn send_filter(&self, filter: WildcardFilter) -> Result<()> {
self.handles
.filters
.send(Command::AddFilter(Box::new(filter)))
}
/// Tests the given url to see if it issues a wildcard response
///
/// In the event that url returns a wildcard response, a
/// [WildcardFilter](struct.WildcardFilter.html) is created and sent to the filters event
/// handler.
///
/// Returns the number of times to increment the caller's progress bar
pub async fn wildcard(&self, target_url: &str) -> Result<u64> {
log::trace!("enter: wildcard_test({:?})", target_url);
if self.handles.config.dont_filter {
// early return, dont_filter scans don't need tested
log::trace!("exit: wildcard_test -> 0");
return Ok(0);
}
let data = match self.handles.config.data.is_empty() {
true => None,
false => Some(self.handles.config.data.as_slice()),
};
let ferox_url = FeroxUrl::from_string(target_url, self.handles.clone());
for method in self.handles.config.methods.iter() {
let ferox_response = self
.make_wildcard_request(&ferox_url, method.as_str(), data, 1)
.await?;
// found a wildcard response
let mut wildcard = WildcardFilter::new(self.handles.config.dont_filter);
let wc_length = ferox_response.content_length();
if wc_length == 0 {
log::trace!("exit: wildcard_test -> 1");
print_dont_filter_message(
WildcardType::Static(&wildcard),
self.handles.config.output_level,
);
self.send_filter(wildcard)?;
return Ok(1);
}
// content length of wildcard is non-zero, perform additional tests:
// make a second request, with a known-sized (64) longer request
let resp_two = self
.make_wildcard_request(&ferox_url, method.as_str(), data, 3)
.await?;
let wc2_length = resp_two.content_length();
wildcard.method = resp_two.method().as_str().to_owned();
if wc2_length == wc_length + (UUID_LENGTH * 2) {
// second length is what we'd expect to see if the requested url is
// reflected in the response along with some static content; aka custom 404
wildcard.dynamic = wc_length - UUID_LENGTH;
print_dont_filter_message(
WildcardType::Dynamic(&wildcard),
self.handles.config.output_level,
);
} else if wc_length == wc2_length {
wildcard.size = wc_length;
print_dont_filter_message(
WildcardType::Static(&wildcard),
self.handles.config.output_level,
);
}
self.send_filter(wildcard)?;
}
log::trace!("exit: wildcard_test");
Ok(2)
}
/// Generates a uuid and appends it to the given target url. The reasoning is that the randomly
/// generated unique string should not exist on and be served by the target web server.
///
/// Once the unique url is created, the request is sent to the server. If the server responds
/// back with a valid status code, the response is considered to be a wildcard response. If that
/// wildcard response has a 3xx status code, that redirection location is displayed to the user.
async fn make_wildcard_request(
&self,
target: &FeroxUrl,
method: &str,
data: Option<&[u8]>,
length: usize,
) -> Result<FeroxResponse> {
log::trace!("enter: make_wildcard_request({}, {})", target, length);
let unique_str = self.unique_string(length);
// To take care of slash when needed
let slash = if self.handles.config.add_slash {
Some("/")
} else {
None
};
let nonexistent_url = target.format(&unique_str, slash)?;
let response = logged_request(
&nonexistent_url.to_owned(),
method,
data,
self.handles.clone(),
)
.await?;
if self
.handles
.config
.status_codes
.contains(&response.status().as_u16())
{
// found a wildcard response
let mut ferox_response = FeroxResponse::from(
response,
&target.target,
method,
self.handles.config.output_level,
)
.await;
ferox_response.set_wildcard(true);
if self
.handles
.filters
.data
.should_filter_response(&ferox_response, self.handles.stats.tx.clone())
{
bail!("filtered response")
}
if matches!(
self.handles.config.output_level,
OutputLevel::Default | OutputLevel::Quiet
) {
let boxed = Box::new(ferox_response.clone());
self.handles.output.send(Command::Report(boxed))?;
}
log::trace!("exit: make_wildcard_request -> {}", ferox_response);
return Ok(ferox_response);
}
log::trace!("exit: make_wildcard_request -> Err");
bail!("uninteresting status code")
}
/// Simply tries to connect to all given sites before starting to scan
///
/// In the event that no sites can be reached, the program will exit.
@@ -439,19 +241,17 @@ impl HeuristicTests {
}
/// given a target's base url, attempt to automatically detect its 404 response
/// pattern, and then set a filter that will exclude all but the first result
pub async fn detect_404_response(&self, target_url: &str) -> Result<u64> {
log::trace!("enter: detect_404_response({:?})", target_url);
/// pattern(s), and then set filters that will exclude those patterns from future
/// responses
pub async fn detect_404_like_responses(&self, target_url: &str) -> Result<u64> {
log::trace!("enter: detect_404_like_responses({:?})", target_url);
if self.handles.config.dont_filter {
// early return, dont_filter scans don't need tested
log::trace!("exit: detect_404_response -> dont_filter is true");
log::trace!("exit: detect_404_like_responses -> dont_filter is true");
return Ok(0);
}
let mut size_sentry = true;
let mut word_sentry = true;
let mut line_sentry = true;
let mut req_counter = 0;
let data = if self.handles.config.data.is_empty() {
@@ -460,24 +260,33 @@ impl HeuristicTests {
Some(self.handles.config.data.as_slice())
};
// To take care of slash when needed
let slash = if self.handles.config.add_slash {
Some("/")
} else {
None
};
// 4 is due to the array in the nested for loop below
let mut responses = Vec::with_capacity(4);
// for every method, attempt to id its 404 response
//
// a good example of one where the GET/POST differ is on hackthebox:
// - http://prd.m.rendering-api.interface.htb/api
for method in self.handles.config.methods.iter() {
for (prefix, length) in [("", 1), ("", 3), (".htaccess", 1), ("admin", 1)] {
let path = format!("{prefix}{}", self.unique_string(length));
// To take care of slash when needed
let slash = if self.handles.config.add_slash {
Some("/")
} else {
None
};
let ferox_url = FeroxUrl::from_string(target_url, self.handles.clone());
let nonexistent_url = ferox_url.format(&path, slash)?;
// example requests:
// - http://localhost/2fc1077836ad43ab98b7a31c2ca28fea
// - http://localhost/92969beae6bf4beb855d1622406d87e395c87387a9ad432e8a11245002b709b03cf609d471004154b83bcc1c6ec49f6f
// - http://localhost/.htaccessa005a2131e68449aa26e99029c914c09
// - http://localhost/adminf1d2541e73c44dcb9d1fb7d93334b280
let response =
logged_request(&nonexistent_url, &method, data, self.handles.clone()).await;
@@ -512,74 +321,18 @@ impl HeuristicTests {
if responses.len() < 2 {
// don't have enough responses to make a determination, continue to next method
responses.clear();
continue;
}
// examine chars/words/lines for each response
// if all responses respetive length match each other, we can assume
// that will remain true for subsequent non-existent urls, and create
// a filter for it.
//
// values are examined from most to least specific (content length, word count, line count)
let content_length = responses[0].content_length();
let word_count = responses[0].word_count();
let line_count = responses[0].line_count();
for response in &responses[1..] {
// if any of the responses differ in length, that particular
// response length type is no longer a canidate for filtering
if response.content_length() != content_length {
size_sentry = false;
}
if response.word_count() != word_count {
word_sentry = false;
}
if response.line_count() != line_count {
line_sentry = false;
}
}
// the if/else-if/else nature of the block means that we'll get the most
// specific match, if one is to be had
//
// each block returns the information needed to send the filter away and
// display a message to the user
let (command, filter_type, filter_length) = if size_sentry {
log::info!(
"[404-like] {target_url} => filtering future responses with {content_length} bytes"
);
(
Command::AddFilter(Box::new(SizeFilter { content_length })),
"bytes",
content_length as usize,
)
} else if word_sentry {
log::info!(
"[404-like] {target_url} => filtering future responses with {word_count} words"
);
(
Command::AddFilter(Box::new(WordsFilter { word_count })),
"words",
word_count,
)
} else if line_sentry {
log::info!(
"[404-like] {target_url} => filtering future responses with {line_count} lines"
);
(
Command::AddFilter(Box::new(LinesFilter { line_count })),
"lines",
line_count,
)
} else {
log::trace!("exit: detect_404_response -> no filter added");
// no match was found; clear the vec and continue to the next
// Command::AddFilter, &str (bytes/words/lines), usize (i.e. length associated with the type)
let Some((command, filter_type, filter_length)) = self.examine_404_like_responses(&responses) else {
// no match was found during analysis of responses
responses.clear();
continue;
};
// check whether we already know about this filter
match command {
Command::AddFilter(ref filter) => {
if let Ok(guard) = self.handles.filters.data.filters.read() {
@@ -590,10 +343,14 @@ impl HeuristicTests {
}
}
}
_ => {}
_ => unreachable!(),
}
// if we're here, we've detected a 404-like response pattern, so we're already filtering for size/word/line
// create the new filter
self.handles.filters.send(command)?;
// if we're here, we've detected a 404-like response pattern, and we're already filtering for size/word/line
//
// in addition, we'll create a similarity filter as a fallback
let hash = SIM_HASHER.create_signature(preprocess(responses[0].text()).iter());
@@ -602,14 +359,14 @@ impl HeuristicTests {
original_url: responses[0].url().to_string(),
};
self.handles.filters.send(command)?;
self.handles
.filters
.send(Command::AddFilter(Box::new(sim_filter)))?;
// reset the responses for the next method, if it exists
responses.clear();
self.handles
.filters
.send(Command::AddFilter(Box::new(sim_filter)))?;
// report to the user, if appropriate
if matches!(
self.handles.config.output_level,
OutputLevel::Default | OutputLevel::Quiet
@@ -619,10 +376,75 @@ impl HeuristicTests {
}
}
log::trace!("exit: detect_404_response");
log::trace!("exit: detect_404_like_responses");
return Ok(req_counter);
}
/// for all responses, examine chars/words/lines
/// if all responses respective lengths match each other, we can assume
/// that will remain true for subsequent non-existent urls
///
/// values are examined from most to least specific (content length, word count, line count)
fn examine_404_like_responses(
&self,
responses: &[FeroxResponse],
) -> Option<(Command, &'static str, usize)> {
let mut size_sentry = true;
let mut word_sentry = true;
let mut line_sentry = true;
let content_length = responses[0].content_length();
let word_count = responses[0].word_count();
let line_count = responses[0].line_count();
for response in &responses[1..] {
// if any of the responses differ in length, that particular
// response length type is no longer a candidate for filtering
if response.content_length() != content_length {
size_sentry = false;
}
if response.word_count() != word_count {
word_sentry = false;
}
if response.line_count() != line_count {
line_sentry = false;
}
}
// the if/else-if/else nature of the block means that we'll get the most
// specific match, if one is to be had
//
// each block returns the information needed to send the filter away and
// display a message to the user
if size_sentry {
// - command to send to the filters handler
// - the unit-type we're filtering on (bytes/words/lines)
// - the value associated with the unit-type on which we're filtering
Some((
Command::AddFilter(Box::new(SizeFilter { content_length })),
"bytes",
content_length as usize,
))
} else if word_sentry {
Some((
Command::AddFilter(Box::new(WordsFilter { word_count })),
"words",
word_count,
))
} else if line_sentry {
Some((
Command::AddFilter(Box::new(LinesFilter { line_count })),
"lines",
line_count,
))
} else {
// no match was found; clear the vec and continue to the next
None
}
}
}
#[cfg(test)]

View File

@@ -3,7 +3,7 @@ use super::*;
use crate::event_handlers::Handles;
use crate::filters::{
EmptyFilter, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter,
WildcardFilter, WordsFilter,
WordsFilter,
};
use crate::traits::FeroxFilter;
use crate::Command::AddFilter;
@@ -182,10 +182,6 @@ impl FeroxScans {
serde_json::from_value::<WordsFilter>(filter.clone())
{
Box::new(deserialized)
} else if let Ok(deserialized) =
serde_json::from_value::<WildcardFilter>(filter.clone())
{
Box::new(deserialized)
} else if let Ok(deserialized) =
serde_json::from_value::<SizeFilter>(filter.clone())
{

View File

@@ -298,25 +298,12 @@ impl FeroxScanner {
// the server is using to respond to resources that don't exist (could be a
// traditional 404, or a custom response)
//
// `detect_404_response` will make the requests that the wildcard test used to
// perform pre-2.8, and then hand those off to `wildcard`, so we're not
// duplicating effort.
//
// the wildcard test will only add a filter in the event that the filter it
// would create isn't already being filtered by the user or by the
// auto-detected 404-like response
let num_reqs_made = test.detect_404_response(&self.target_url).await?;
// `detect_404_like_responses` will make the requests that the wildcard test used to
// perform pre-2.8 in addition to new detection techniques, superseding the old
// wildcard test
let num_reqs_made = test.detect_404_like_responses(&self.target_url).await?;
// let num_reqs_made = test.wildcard(detection_responses).await.unwrap_or_default();
progress_bar.inc(num_reqs_made);
// if !detected {
// // on error, we'll have 0, same for --dont-filter
// // anything higher than 0 indicates a wildcard was found
// let num_reqs_made = test.wildcard(&self.target_url).await.unwrap_or_default();
// progress_bar.inc(num_reqs_made);
// }
}
// Arc clones to be passed around to the various scans

View File

@@ -1,6 +1,6 @@
//! collection of all traits used
use crate::filters::{
LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter, WildcardFilter,
LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter,
WordsFilter,
};
use crate::response::FeroxResponse;
@@ -37,12 +37,6 @@ impl Display for dyn FeroxFilter {
write!(f, "Response size: {}", style(filter.content_length).cyan())
} else if let Some(filter) = self.as_any().downcast_ref::<RegexFilter>() {
write!(f, "Regex: {}", style(&filter.raw_string).cyan())
} else if let Some(filter) = self.as_any().downcast_ref::<WildcardFilter>() {
if filter.dynamic != u64::MAX {
write!(f, "Dynamic wildcard: {}", style(filter.dynamic).cyan())
} else {
write!(f, "Static wildcard: {}", style(filter.size).cyan())
}
} else if let Some(filter) = self.as_any().downcast_ref::<StatusCodeFilter>() {
write!(f, "Status code: {}", style(filter.filter_code).cyan())
} else if let Some(filter) = self.as_any().downcast_ref::<SimilarityFilter>() {