Upgrade binary test fixtures management (#2444)

* test: strip fixtures of any execution permissions

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* chore: add lint check for large files

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* add helper script to capture binary snippets

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* chore: update scripts and add new dir output for snippets

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* test: update erlang test to new generated format

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* test: update memcached to new generator pattern

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* test: update openjdk to named version

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* test: move openjdk lts to versioned folder

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* test: rename unversioned java to versioned folders

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* test: migrate bash fixture to new snippet workflow

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* test: update script to size 600 bytes

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* test: update go classifier to new snippet workflow

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* test: move haproxy new new snippet

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* test: add flatter haproxy example

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* test: update tests to new pattern

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* test: final version of snippet script

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>

* [wip] download bin helpers

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add manager for binary cataloger test fixtures

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add remaining binary cataloger patterns and snippets

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* adjust gitignore to be more permissive to snippets

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add rust darwin snippets

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* skip tests that are missing full binaries

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* address PR feedback

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add tests for binary test fixture manager

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* highlight rows that do not have binaries or snippets

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* bump fixture limit to 1K (found exceptions when adding snippets)

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add redis and postgres snippets

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* improve formating of fixture listing

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Christopher Angelo Phillips
2024-01-05 21:40:03 +00:00
committed by GitHub
co-authored by Alex Goodman
parent a16a4ad6c9
commit 7182f5b519
158 changed files with 3539 additions and 500 deletions
@@ -1 +1,10 @@
classifiers/dynamic
classifiers/bin
cache.fingerprint
# allow for lb patterns (rust, pytho, php and more)
!lib*.so
!lib*.dylib
# allow for go-hint file
!VERSION*
+19 -100
View File
@@ -1,110 +1,29 @@
.PHONY: all
all: \
classifiers/dynamic/python-binary-shared-lib-3.11 \
classifiers/dynamic/python-binary-shared-lib-redhat-3.9 \
classifiers/dynamic/python-binary-with-version-3.9 \
classifiers/dynamic/python-binary-3.4-alpine \
classifiers/dynamic/ruby-library-3.2.1 \
classifiers/dynamic/ruby-library-2.7.7 \
classifiers/dynamic/ruby-library-2.6.10 \
classifiers/dynamic/helm-3.11.1 \
classifiers/dynamic/helm-3.10.3 \
classifiers/dynamic/consul-1.15.2
.PHONY: default list download download-all cache.fingerprint
.DEFAULT_GOAL := default
default: download
classifiers/dynamic/python-binary-shared-lib-3.11:
$(eval $@_image := "python:3.11-slim@sha256:0b106e1d2bf485c2a41474bc9cd5103e9eea4e179f40f10741b53b127059221e")
./get-image-file.sh $($@_image) \
/usr/local/bin/python3.11 \
$@/python3
./get-image-file.sh $($@_image) \
/usr/local/lib/libpython3.11.so.1.0 \
$@/libpython3.11.so.1.0
list: ## list all managed binaries and snippets
go run ./manager list
classifiers/dynamic/python-binary-shared-lib-redhat-3.9:
$(eval $@_image := "registry.access.redhat.com/ubi8/python-39@sha256:f3cf958b96ce016b63e3e163e488f52e42891304dafef5a0811563f22e3cbad0")
./get-image-file.sh $($@_image) \
/usr/bin/python3.9 \
$@/python3.9
./get-image-file.sh $($@_image) \
/usr/lib64/libpython3.9.so.1.0 \
$@/libpython3.9.so.1.0
download: ## download only binaries that are not covered by a snippet
go run ./manager download $(name) --skip-if-covered-by-snippet
classifiers/dynamic/python-binary-with-version-3.9:
$(eval $@_image := "python:3.9.16-bullseye@sha256:93fb93c461a2e47a2176706fad1f39eaacd5dd40e19c0b018699a28c03eb2e2a")
./get-image-file.sh $($@_image) \
/usr/bin/python3.9 \
$@/python3.9
download-all: ## download all managed binaries
go run ./manager download
classifiers/dynamic/python-binary-3.4-alpine:
$(eval $@_image := "python:3.4-alpine@sha256:c210b660e2ea553a7afa23b41a6ed112f85dbce25cbcb567c75dfe05342a4c4b")
./get-image-file.sh $($@_image) \
/usr/local/bin/python3.4 \
$@/python3.4
./get-image-file.sh $($@_image) \
/usr/local/lib/libpython3.4m.so.1.0 \
$@/libpython3.4m.so.1.0
add-snippet: ## add a new snippet from an existing binary
go run ./manager add-snippet
classifiers/dynamic/ruby-library-3.2.1:
$(eval $@_image := "ruby:3.2.1-bullseye@sha256:b4a140656b0c5d26c0a80559b228b4d343f3fdbf56682fcbe88f6db1fa9afa6b")
./get-image-file.sh $($@_image) \
/usr/local/bin/ruby \
$@/ruby
./get-image-file.sh $($@_image) \
/usr/local/lib/libruby.so.3.2.1 \
$@/libruby.so.3.2.1
./get-image-file.sh $($@_image) \
/usr/local/lib/libruby.so.3.2 \
$@/libruby.so.3.2
cache.fingerprint: ## prints the sha256sum of the any input to the download command (to determine if there is a cache miss)
@cat ./config.yaml | sha256sum | awk '{print $$1}' | tee cache.fingerprint
classifiers/dynamic/ruby-library-2.7.7:
$(eval $@_image := "ruby:2.7.7-bullseye@sha256:055191740a063f33fef1f09423e5ed8f91143aae62a3772a90910118464c5120")
./get-image-file.sh $($@_image) \
/usr/local/bin/ruby \
$@/ruby
./get-image-file.sh $($@_image) \
/usr/local/lib/libruby.so.2.7.7 \
$@/libruby.so.2.7.7
./get-image-file.sh $($@_image) \
/usr/local/lib/libruby.so.2.7 \
$@/libruby.so.2.7
clean: ## clean up all downloaded binaries
rm -rf ./classifiers/bin
classifiers/dynamic/ruby-library-2.6.10:
$(eval $@_image := "ruby:2.6.10@sha256:771a810704167e55da8a19970c5dfa6eb795dfee32547adffa29ea72703f7243")
./get-image-file.sh $($@_image) \
/usr/local/bin/ruby \
$@/ruby
./get-image-file.sh $($@_image) \
/usr/local/lib/libruby.so.2.6.10 \
$@/libruby.so.2.6.10
./get-image-file.sh $($@_image) \
/usr/local/lib/libruby.so.2.6 \
$@/libruby.so.2.6
## Halp! #################################
classifiers/dynamic/helm-3.11.1:
$(eval $@_image := "alpine/helm:3.11.1@sha256:8628e3695fb743a8b9de89626f1b7a221280c2152c0e288c2504e59b68233e8b")
./get-image-file.sh $($@_image) \
/usr/bin/helm \
$@/helm
classifiers/dynamic/helm-3.10.3:
$(eval $@_image := "argoproj/argocd:v2.6.4@sha256:61fcbba187ff53c00696cb580edf70cada59c45cf399d8477631acf43cf522ee")
./get-image-file.sh $($@_image) \
/usr/local/bin/helm \
$@/helm
classifiers/dynamic/consul-1.15.2:
$(eval $@_image := "hashicorp/consul:1.15.2@sha256:c2169f3bb18dd947ae8eb5f6766896695c71fb439f050a3343e0007d895615b8")
./get-image-file.sh $($@_image) \
/bin/consul \
$@/consul
.PHONY: clean
clean:
rm -rf classifiers/dynamic
.PHONY: cache.fingerprint
cache.fingerprint: # for CI
$(title,Install test fixture fingerprint)
@find ./classifiers/dynamic/* -type f -exec md5sum {} + | awk '{print $1}' | sort | tee /dev/stderr | md5sum | tee cache.fingerprint >> cache.fingerprint
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}'
+149
View File
@@ -0,0 +1,149 @@
#!/bin/bash
# Default values for length and prefix length
LENGTH=100
PREFIX_LENGTH=20
SEARCH_FOR=''
GROUP_NAME=''
# Function to show usage
usage() {
echo "Usage: $0 <path-to-binary> <version> [--search-for <pattern>] [--length <length>] [--prefix-length <prefix_length>] [--group <name>]"
exit 1
}
# Parse arguments
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
--search-for)
SEARCH_FOR="$2"
shift # past argument
shift # past value
;;
--length)
LENGTH="$2"
shift # past argument
shift # past value
;;
--group)
GROUP_NAME="$2"
shift # past argument
shift # past value
;;
--prefix-length)
PREFIX_LENGTH="$2"
shift # past argument
shift # past value
;;
*)
if [ -z "$BINARY_FILE" ]; then
BINARY_FILE="$1"
elif [ -z "$VERSION" ]; then
VERSION="$1"
else
echo "Unknown option: $1"
usage
fi
shift # past argument
;;
esac
done
LENGTH=$(expr "$LENGTH" + "$PREFIX_LENGTH")
# check if binary file and pattern are provided
if [ -z "$BINARY_FILE" ] || [ -z "$VERSION" ]; then
usage
fi
# if group name is empty use the binary filename
if [ -z "$GROUP_NAME" ]; then
GROUP_NAME=$(basename "$BINARY_FILE")
fi
# check if xxd is even installed
if ! command -v xxd &> /dev/null; then
echo "xxd not found. Please install xxd."
exit 1
fi
PATTERN=${SEARCH_FOR:-$VERSION}
echo "Using binary file: $BINARY_FILE"
echo "Searching for pattern: $PATTERN"
echo "Capture length: $LENGTH bytes"
echo "Capture prefix length: $PREFIX_LENGTH bytes"
PATTERN_RESULTS=$(strings -t d "$BINARY_FILE" | grep "$PATTERN")
RESULT_COUNT=$(echo "$PATTERN_RESULTS" | wc -l)
CONTINUE_LOOP=true
while $CONTINUE_LOOP; do
# if there are multiple matches, prompt the user to select one
if [ $RESULT_COUNT -gt 1 ]; then
echo "Multiple string matches found in the binary:"
echo ""
# show result lines one at a time (in a numbered list)
# but only show everything after the first field (not the offset)
echo "$PATTERN_RESULTS" | cut -d ' ' -f 2- | nl -w 1 -s ') '
echo ""
read -p "Please select a match: " SELECTION
# if the selection is not a number, exit
if ! [[ "$SELECTION" =~ ^[0-9]+$ ]]; then
echo "Invalid selection."
exit 1
fi
# if the selection is out of bounds, exit
if [ "$SELECTION" -gt $(echo "$PATTERN_RESULTS" | wc -l) ]; then
echo "Invalid selection."
exit 1
fi
# select the line from the results
SELECTED_RESULT=$(echo "$PATTERN_RESULTS" | sed -n "${SELECTION}p")
else
SELECTED_RESULT="$PATTERN_RESULTS"
fi
# search for the pattern in the binary file and capture the offset
OFFSET=$(echo "${SELECTED_RESULT}" | cut -d ' ' -f 1)
if [ -z "$OFFSET" ]; then
echo "Pattern not found."
exit 1
fi
# adjust the offset to capture prefix length before the match
OFFSET=$(expr "$OFFSET" - "$PREFIX_LENGTH")
# use xxd to capture the specified length from the calculated offset
SNIPPET=$(xxd -l "$LENGTH" -s "$OFFSET" "$BINARY_FILE")
# display the output and prompt the user
echo ""
echo "$SNIPPET"
echo ""
read -p "Does this snippet capture what you need? (Y/n/q) " RESPONSE
RESPONSE=${RESPONSE:-y}
if [ "$RESPONSE" == "y" ]; then
CONTINUE_LOOP=false
elif [ $RESULT_COUNT -eq 1 ] || [ "$RESPONSE" == "q" ]; then
echo "Exiting with no action taken."
exit 0
fi
done
go run ./manager write-snippet "$BINARY_FILE" --offset "$OFFSET" --length "$LENGTH" --name "$GROUP_NAME" --version "$VERSION"
@@ -1,2 +0,0 @@
!*.so
!VERSION
@@ -1 +0,0 @@
@(#)Bash version 5.2.15(1) release GNU
@@ -1,3 +0,0 @@
# note: this SHOULD match as busybox 3.33.3
noise!BusyBox v3.33.3!noise
@@ -1 +0,0 @@
HA-Proxy version 1.5.14 2015/07/02Copyright 2000-2015 Willy Tarreau <willy@haproxy.org>
@@ -1,18 +0,0 @@
<Directory "
mailto:
Apache/2.4.54
Server at <a href="
<address>
--
</a> Port
Server at
Apache/2.4.54 (Unix)
ap_logio_add_bytes_out
ap_ident_lookup
--
access_compat_ap_satisfies
Apache
Apache/2.4
Apache/2
<VirtualHost
<Limit
@@ -1,31 +0,0 @@
The binary snippet was gathered with:
```bash
$ cat ./original-mariadb | strings | grep '-MariaDB'
# assert you can see the value
$ xxd ./original-mariadb | grep '\-MariaDB'
# get the address...
$ xxd -s 0x003dd5c0 -l 40 ./original-mariadb
003dd5c0: 2900 4c69 6e75 7800 3130 2e36 2e31 352d ).Linux.10.6.15-
003dd5d0: 4d61 7269 6144 4200 7265 6164 6c69 6e65 MariaDB.readline
003dd5e0: 0078 3836 5f36 3400 .x86_64.
$ dd if=./original-mariadb of=mariadb bs=1 skip=$((0x003dd5c0)) count=40
40+0 records in
40+0 records out
40 bytes transferred in 0.000264 secs (151515 bytes/sec)
$ xxd mariadb
00000000: 2900 4c69 6e75 7800 3130 2e36 2e31 352d ).Linux.10.6.15-
00000010: 4d61 7269 6144 4200 7265 6164 6c69 6e65 MariaDB.readline
00000020: 0078 3836 5f36 3400 .x86_64.
```
@@ -1,13 +0,0 @@
secs_since_last_cmd
listen
memcached 1.6.18
udp-port
memcached 1.6.18
Failed to allocate memory
%s,%s
--
bget
flush_all
VERSION 1.6.18
quit
BADCLASS invalid class id
@@ -1,39 +0,0 @@
The binary snippet was gathered with:
```bash
$ cat ./original-mysql | strings | grep '5.6.51'
# assert you can see the value
$ xxd ./original-mysql | grep '5.6.51'
# get the address...
$ xxd -s 0x008f13d0 -l 100 original-mysql
008f13d0: 2d62 6163 6b75 702d 7265 7374 6f72 6572 -backup-restorer
008f13e0: 2d6d 7973 716c 2d35 2e36 2f6d 7973 716c -mysql-5.6/mysql
008f13f0: 2d35 2e36 2e35 312f 636c 6965 6e74 2f63 -5.6.51/client/c
008f1400: 6f6d 706c 6574 696f 6e5f 6861 7368 2e63 ompletion_hash.c
008f1410: 6300 2f76 6172 2f76 6361 702f 6461 7461 c./var/vcap/data
008f1420: 2f63 6f6d 7069 6c65 2f64 6174 6162 6173 /compile/databas
008f1430: 652d 6261 e-ba
$ dd if=./original-mysql of=mysql bs=1 skip=$((0x008f13d0)) count=100
100+0 records in
100+0 records out
100 bytes transferred in 0.000642 secs (155763 bytes/sec)
$ xxd mysql
00000000: 2d62 6163 6b75 702d 7265 7374 6f72 6572 -backup-restorer
00000010: 2d6d 7973 716c 2d35 2e36 2f6d 7973 716c -mysql-5.6/mysql
00000020: 2d35 2e36 2e35 312f 636c 6965 6e74 2f63 -5.6.51/client/c
00000030: 6f6d 706c 6574 696f 6e5f 6861 7368 2e63 ompletion_hash.c
00000040: 6300 2f76 6172 2f76 6361 702f 6461 7461 c./var/vcap/data
00000050: 2f63 6f6d 7069 6c65 2f64 6174 6162 6173 /compile/databas
00000060: 652d 6261 e-ba
```
@@ -1,40 +0,0 @@
The binary snippet was gathered with:
```bash
$ cat ./original-mysql | strings | grep '8.0.34'
# assert you can see the value
$ xxd ./original-mysql | grep '8.0.34'
# get the address...
$ xxd -s 0x0014cd20 -l 100 original-mysql
0014cd20: 2069 7320 616c 7265 6164 7920 6c6f 6164 is already load
0014cd30: 6564 0000 0000 0000 2e2e 2f2e 2e2f 6d79 ed......../../my
0014cd40: 7371 6c2d 382e 302e 3334 2f73 716c 2d63 sql-8.0.34/sql-c
0014cd50: 6f6d 6d6f 6e2f 636c 6965 6e74 5f70 6c75 ommon/client_plu
0014cd60: 6769 6e2e 6363 002f 7573 722f 6c6f 6361 gin.cc./usr/loca
0014cd70: 6c2f 6d79 7371 6c2f 6c69 622f 706c 7567 l/mysql/lib/plug
0014cd80: 696e 0049 in.I
$ dd if=./original-mysql of=mysql bs=1 skip=$((0x0014cd20)) count=100
100+0 records in
100+0 records out
100 bytes transferred in 0.000519 secs (192678 bytes/sec)
$ xxd mysql
00000000: 2069 7320 616c 7265 6164 7920 6c6f 6164 is already load
00000010: 6564 0000 0000 0000 2e2e 2f2e 2e2f 6d79 ed......../../my
00000020: 7371 6c2d 382e 302e 3334 2f73 716c 2d63 sql-8.0.34/sql-c
00000030: 6f6d 6d6f 6e2f 636c 6965 6e74 5f70 6c75 ommon/client_plu
00000040: 6769 6e2e 6363 002f 7573 722f 6c6f 6361 gin.cc./usr/loca
00000050: 6c2f 6d79 7371 6c2f 6c69 622f 706c 7567 l/mysql/lib/plug
00000060: 696e 0049 in.I
```
@@ -1,2 +0,0 @@
# this should match node 19.2.1
node.js/v19.2.1
Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 B

@@ -1,5 +0,0 @@
[null] Script: '%s'
%s,%s
X-Powered-By: PHP/8.2.1
index pointer
PHP_VERSION
@@ -1,5 +0,0 @@
[null] Script: '%s'
%s,%s
X-Powered-By: PHP/8.2.1
index pointer
PHP_VERSION
@@ -1,5 +0,0 @@
[null] Script: '%s'
%s,%s
X-Powered-By: PHP/8.2.1
index pointer
PHP_VERSION
@@ -1,5 +0,0 @@
byteaout
byteain
?PostgreSQL 9.5alpha1 on x86_64-unknown-linux-gnu, compiled by gcc (Debian 4.9.2-10) 4.9.2, 64-bit
/tmp/buildd/postgresql-9.5-9.5~alpha1/build/../src/backend/utils/adt/windowfuncs.c
argument of ntile must be greater than zero
@@ -1,9 +0,0 @@
super from singleton method that is defined to multiple classes is not supported; this will be fixed in 1.9.3 or later
/usr/local/lib/ruby/site_ruby/1.9.1
/usr/local/lib/ruby/site_ruby/1.9.1/x86_64-linux
/usr/local/lib/ruby/vendor_ruby/1.9.1
/usr/local/lib/ruby/vendor_ruby/1.9.1/x86_64-linux
/usr/local/lib/ruby/1.9.1
/usr/local/lib/ruby/1.9.1/x86_64-linux
ruby 1.9.3p551 (2014-11-13 revision 48407) [x86_64-linux]
1.9.3
@@ -0,0 +1,12 @@
name: consul
offset: 57272433
length: 100
snippetSha256: a4a955b180d6df471797a9f17f5ebf6f23b92d688d40532712683922e119dac0
fileSha256: 5c5fed218247eaf43c3b54008b6a4c5d5cfa1b38539d6e7bfc09ac04623389fc
### byte snippet to follow ###
e%7D" />
<!-- CONSUL_VERSION: 1.15.2
-->
<link rel="icon" href="{{.ContentPath}}assets/favicon
@@ -0,0 +1,8 @@
name: mysql
offset: 1377134
length: 100
snippetSha256: f18b2e276e188db350b2d5e580085db8f8d5ae28f1eb9413523a8ea5948cb981
fileSha256: 38ba0547af106c7ccd78e98233499530d50eaa0c1aaea0f46002b03f28992beb
### byte snippet to follow ###
íÿ`íÿz íÿ/var/lib/pb2/sb_1-11875240-1687434163.06/rpm/BUILD/mysql-8.0.34/mysql-8.0.34/sql-common/cl
@@ -0,0 +1,12 @@
name: libphp.so
offset: unknown
length: unknown
snippetSha256: d39ac8dadf5ba868455c487f1d0bb4c8bec64006fd7e5d76e3e27a26e47e637f
fileSha256: unknown
### byte snippet to follow ###
%s'
%s,%s
X-Powered-By: PHP/8.2.1
index pointer
PHP_VERSION
@@ -0,0 +1,12 @@
name: php
offset: unknown
length: unknown
snippetSha256: d39ac8dadf5ba868455c487f1d0bb4c8bec64006fd7e5d76e3e27a26e47e637f
fileSha256: unknown
### byte snippet to follow ###
%s'
%s,%s
X-Powered-By: PHP/8.2.1
index pointer
PHP_VERSION
@@ -0,0 +1,12 @@
name: php-fpm
offset: unknown
length: unknown
snippetSha256: d39ac8dadf5ba868455c487f1d0bb4c8bec64006fd7e5d76e3e27a26e47e637f
fileSha256: unknown
### byte snippet to follow ###
%s'
%s,%s
X-Powered-By: PHP/8.2.1
index pointer
PHP_VERSION
@@ -0,0 +1,10 @@
name: postgres
offset: unknown
length: unknown
snippetSha256: 8f8323161ded8fd737ba7157b36b794c55354e8a9b8b6c5aa07dab9c3468886c
fileSha256: unknown
### byte snippet to follow ###
t
byteain
?PostgreSQL 9.5alpha1 on x86_64-unknown-linux-gnu, compiled by gcc (Debian 4.9.2-10) 4.9.2

Some files were not shown because too many files have changed in this diff Show More