├── static ├── logo.png ├── 41125338.png ├── lc-httpx.png ├── teamssix.png ├── wgpsec.png ├── 39155974.jpeg ├── 49087564.jpeg ├── buy-coffee.png └── list-cloud-run.png ├── utils ├── const.go └── utils.go ├── CHANGELOG.md ├── cmd ├── banner.go ├── configFile.go ├── runner.go └── options.go ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── perf.yml │ ├── feat.yml │ └── bug.yml └── workflows │ └── release.yaml ├── pkg ├── providers │ ├── tianyi │ │ ├── oos.go │ │ └── tianyi.go │ ├── huawei │ │ ├── obs.go │ │ └── huawei.go │ ├── baidu │ │ ├── bos.go │ │ ├── bcc.go │ │ └── baidu.go │ ├── tencent │ │ ├── cos.go │ │ ├── lh.go │ │ ├── cvm.go │ │ └── tencent.go │ ├── qiniu │ │ ├── kodo.go │ │ └── qiniu.go │ ├── aliyun │ │ ├── domain.go │ │ ├── oss.go │ │ ├── fc3.go │ │ ├── ecs.go │ │ ├── rds.go │ │ ├── aliyun.go │ │ └── fc.go │ ├── yidong │ │ ├── yidong.go │ │ └── eos.go │ └── liantong │ │ ├── liantong.go │ │ └── oss.go ├── inventory │ └── inventory.go └── schema │ ├── validate │ └── validate.go │ └── schema.go ├── main.go ├── LICENSE ├── .goreleaser.yaml ├── CONTRIBUTING.md ├── go.mod ├── README.md └── go.sum /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wgpsec/lc/HEAD/static/logo.png -------------------------------------------------------------------------------- /static/41125338.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wgpsec/lc/HEAD/static/41125338.png -------------------------------------------------------------------------------- /static/lc-httpx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wgpsec/lc/HEAD/static/lc-httpx.png -------------------------------------------------------------------------------- /static/teamssix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wgpsec/lc/HEAD/static/teamssix.png -------------------------------------------------------------------------------- /static/wgpsec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wgpsec/lc/HEAD/static/wgpsec.png -------------------------------------------------------------------------------- /static/39155974.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wgpsec/lc/HEAD/static/39155974.jpeg -------------------------------------------------------------------------------- /static/49087564.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wgpsec/lc/HEAD/static/49087564.jpeg -------------------------------------------------------------------------------- /static/buy-coffee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wgpsec/lc/HEAD/static/buy-coffee.png -------------------------------------------------------------------------------- /static/list-cloud-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wgpsec/lc/HEAD/static/list-cloud-run.png -------------------------------------------------------------------------------- /utils/const.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | const ( 4 | Provider = "provider" 5 | Id = "id" 6 | CloudServices = "cloud_services" 7 | AccessKey = "access_key" 8 | SecretKey = "secret_key" 9 | SessionToken = "session_token" 10 | ) 11 | 12 | const ( 13 | Aliyun = "aliyun" 14 | Tencent = "tencent" 15 | Huawei = "huawei" 16 | TianYi = "tianyi" 17 | Baidu = "baidu" 18 | LianTong = "liantong" 19 | QiNiu = "qiniu" 20 | YiDong = "yidong" 21 | ) 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v1.1.0](https://github.com/wgpsec/lc/releases/tag/v1.1.0) 2024.10.6 4 | 5 | * 支持列出阿里云 FC 函数计算服务 6 | * 支持列出阿里云域名服务 7 | * 支持在列出时指定要列出的云服务类型 8 | * 修复阿里云 ECS 在绑定 EIP 时无法列出 IP 的 Bug 9 | 10 | ## [v1.0.1](https://github.com/wgpsec/lc/releases/tag/v1.0.1) 2024.5.15 11 | 12 | * 支持列出移动云 EOS 对象存储服务 13 | * 修复了一个 Bug 14 | 15 | ## [v1.0.0](https://github.com/wgpsec/lc/releases/tag/v1.0.0) 2024.4.20 16 | 17 | * 支持列出阿里云 RDS 数据库服务 18 | * 支持列出腾讯云 COS 对象存储服务 19 | * 支持列出七牛云 Kodo 对象存储服务 20 | * 增加详细日志输出 21 | 22 | ## [v0.0.1](https://github.com/wgpsec/lc/releases/tag/v0.0.1) 2024.4.6 23 | 24 | * 首次提交代码 -------------------------------------------------------------------------------- /cmd/banner.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/projectdiscovery/gologger" 4 | 5 | const banner = ` 6 | __ _ __ ________ __ 7 | / / (_)____/ /_ / ____/ /___ __ ______/ / 8 | / / / / ___/ __/ / / / / __ \/ / / / __ / 9 | / /___/ (__ ) /_ / /___/ / /_/ / /_/ / /_/ / 10 | /_____/_/____/\__/ \____/_/\____/\__,_/\__,_/ 11 | ` 12 | 13 | const version = "1.1.0" 14 | const versionDate = "2024-10-6" 15 | 16 | func showBanner() { 17 | gologger.Print().Msgf("%s\n", banner) 18 | gologger.Print().Msgf("\t %s %s\n\t github.com/wgpsec/lc\n\n", version, versionDate) 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | tmp 20 | test 21 | build 22 | 23 | # Go workspace file 24 | go.work 25 | .DS_Store 26 | .idea 27 | lc 28 | lc.bak 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/perf.yml: -------------------------------------------------------------------------------- 1 | name: 功能优化反馈 2 | description: 提交一个功能优化需求帮助改进这个项目 3 | title: "[Perf] 在这里输入你的标题" 4 | labels: ["performance"] 5 | assignees: 6 | - teamssix 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | 感谢您花时间提交这份 issue 12 | - type: textarea 13 | id: performance 14 | attributes: 15 | label: 描述你希望优化的功能 16 | value: "详细描述你希望优化的功能,并且描述为什么想要对这个功能进行优化,描述的越完善该反馈越有可能被采纳。" 17 | validations: 18 | required: true 19 | - type: textarea 20 | attributes: 21 | label: 补充信息 22 | description: | 23 | 链接?参考资料?任何可以给我们提供更多关于你所遇到的问题的背景资料的东西 24 | 25 | 提示:你可以通过点击这个区域来突出显示它,然后将文件拖入,从而附上图片或其他文件。 26 | validations: 27 | required: false 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feat.yml: -------------------------------------------------------------------------------- 1 | name: 新功能反馈 2 | description: 提交一个功能需求帮助完善这个项目 3 | title: "[Feat] 在这里输入你的标题" 4 | labels: ["enhancement"] 5 | assignees: 6 | - teamssix 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | 感谢您花时间提交这份 issue 12 | - type: textarea 13 | id: new-features 14 | attributes: 15 | label: 描述你希望增加的功能 16 | value: "详细描述你希望增加的功能,并且描述为什么想要增加这个功能以及意义,描述的越完善该反馈越有可能被采纳。" 17 | validations: 18 | required: true 19 | - type: textarea 20 | attributes: 21 | label: 补充信息 (Anything else?) 22 | description: | 23 | 链接?参考资料?任何可以给我们提供更多关于你所遇到的问题的背景资料的东西。 24 | 25 | 提示:你可以通过点击这个区域来突出显示它,然后将文件拖入,从而附上图片或其他文件。 26 | validations: 27 | required: false 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | env: 7 | GO_VERSION: 1.22.1 8 | 9 | jobs: 10 | goreleaser: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Source Code 14 | uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | - name: Setup Go Environment 18 | uses: actions/setup-go@v3 19 | with: 20 | go-version: ${{ env.GO_VERSION }} 21 | 22 | - name: Run GoReleaser 23 | uses: goreleaser/goreleaser-action@v6 24 | with: 25 | version: 1.26.2 26 | args: release --clean 27 | env: 28 | CGO_ENABLED: 0 29 | GITHUB_TOKEN: ${{ secrets.RELEASE_GH_TOKEN }} -------------------------------------------------------------------------------- /pkg/providers/tianyi/oos.go: -------------------------------------------------------------------------------- 1 | package tianyi 2 | 3 | import ( 4 | "context" 5 | "github.com/projectdiscovery/gologger" 6 | "github.com/teamssix/oos-go-sdk/oos" 7 | "github.com/wgpsec/lc/pkg/schema" 8 | "strings" 9 | ) 10 | 11 | type oosProvider struct { 12 | id string 13 | provider string 14 | oosClient *oos.Client 15 | } 16 | 17 | func (d *oosProvider) GetResource(ctx context.Context) (*schema.Resources, error) { 18 | var list = schema.NewResources() 19 | gologger.Debug().Msg("正在获取天翼云 OOS 资源信息") 20 | response, err := d.oosClient.ListBuckets() 21 | if err != nil { 22 | return nil, err 23 | } 24 | for _, bucket := range response.Buckets { 25 | endpointBuilder := &strings.Builder{} 26 | endpointBuilder.WriteString(bucket.Name) 27 | endpointBuilder.WriteString(".oos-cn.ctyunapi.cn") 28 | list.Append(&schema.Resource{ 29 | ID: d.id, 30 | Public: true, 31 | DNSName: endpointBuilder.String(), 32 | Provider: d.provider, 33 | }) 34 | } 35 | return list, nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/providers/huawei/obs.go: -------------------------------------------------------------------------------- 1 | package huawei 2 | 3 | import ( 4 | "context" 5 | "github.com/huaweicloud/huaweicloud-sdk-go-obs/obs" 6 | "github.com/wgpsec/lc/pkg/schema" 7 | "strings" 8 | ) 9 | 10 | type obsProvider struct { 11 | id string 12 | provider string 13 | obsClient *obs.ObsClient 14 | } 15 | 16 | func (d *obsProvider) GetResource(ctx context.Context) (*schema.Resources, error) { 17 | var list = schema.NewResources() 18 | response, err := d.obsClient.ListBuckets(&obs.ListBucketsInput{QueryLocation: true}) 19 | if err != nil { 20 | return nil, err 21 | } 22 | for _, bucket := range response.Buckets { 23 | endpointBuilder := &strings.Builder{} 24 | endpointBuilder.WriteString(bucket.Name) 25 | endpointBuilder.WriteString(".obs." + bucket.Location) 26 | endpointBuilder.WriteString(".myhuaweicloud.com") 27 | list.Append(&schema.Resource{ 28 | ID: d.id, 29 | Public: true, 30 | DNSName: endpointBuilder.String(), 31 | Provider: d.provider, 32 | }) 33 | } 34 | return list, nil 35 | } 36 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // 感谢 github.com/projectdiscovery/cloudlist 项目, 得益 2 | // 于 cloudlist 的 MIT 开源协议,为这个 Weekend Project 提 3 | // 供了大量帮助, 此项目也将以 MIT 协议开源,助力安全开源项目发展。 4 | 5 | // Thank you to the github.com/projectdiscovery/cloudlist 6 | // project, which has provided substantial assistance to this 7 | // Weekend Project thanks to its use of the MIT open-source 8 | // license. As a result, this project will also adopt the MIT 9 | // license, joining forces to promote the development of 10 | // open-source initiatives for the benefit of humanity. 11 | 12 | package main 13 | 14 | import ( 15 | "github.com/projectdiscovery/gologger" 16 | "github.com/wgpsec/lc/cmd" 17 | "io" 18 | ) 19 | 20 | func main() { 21 | options := cmd.ParseOptions() 22 | runner, err := cmd.New(options) 23 | if err != nil { 24 | gologger.Info().Msg("使用 -h 或 --help 参数查看 lc 的帮助信息。") 25 | if err == io.EOF { 26 | gologger.Fatal().Msgf("配置文件为空,请在配置文件中填写上云服务商的访问配置,配置文件地址:%s\n", options.Config) 27 | } else { 28 | gologger.Fatal().Msgf("%s", err) 29 | } 30 | } 31 | runner.Enumerate() 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Wolf Group Security Team 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 8 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /pkg/providers/baidu/bos.go: -------------------------------------------------------------------------------- 1 | package baidu 2 | 3 | import ( 4 | "context" 5 | "github.com/baidubce/bce-sdk-go/services/bos" 6 | "github.com/projectdiscovery/gologger" 7 | "github.com/wgpsec/lc/pkg/schema" 8 | "strings" 9 | ) 10 | 11 | type bosProvider struct { 12 | id string 13 | provider string 14 | bosClient *bos.Client 15 | } 16 | 17 | func (d *bosProvider) GetResource(ctx context.Context) (*schema.Resources, error) { 18 | var list = schema.NewResources() 19 | gologger.Debug().Msg("正在获取百度云 BOS 资源信息") 20 | response, err := d.bosClient.ListBuckets() 21 | if err != nil { 22 | return nil, err 23 | } 24 | for _, bucket := range response.Buckets { 25 | endpointBuilder := &strings.Builder{} 26 | endpointBuilder.WriteString(bucket.Name) 27 | endpointBuilder.WriteString("." + bucket.Location) 28 | endpointBuilder.WriteString(".bcebos.com") 29 | list.Append(&schema.Resource{ 30 | ID: d.id, 31 | Public: true, 32 | DNSName: endpointBuilder.String(), 33 | Provider: d.provider, 34 | }) 35 | } 36 | return list, nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/providers/tencent/cos.go: -------------------------------------------------------------------------------- 1 | package tencent 2 | 3 | import ( 4 | "context" 5 | "github.com/projectdiscovery/gologger" 6 | "github.com/tencentyun/cos-go-sdk-v5" 7 | "github.com/wgpsec/lc/pkg/schema" 8 | "strings" 9 | ) 10 | 11 | type cosProvider struct { 12 | id string 13 | provider string 14 | cosClient *cos.Client 15 | } 16 | 17 | func (d *cosProvider) GetResource(ctx context.Context) (*schema.Resources, error) { 18 | cosList := schema.NewResources() 19 | gologger.Debug().Msg("正在获取腾讯云 COS 资源信息") 20 | response, _, err := d.cosClient.Service.Get(context.Background()) 21 | if err != nil { 22 | return nil, err 23 | } 24 | for _, bucket := range response.Buckets { 25 | endpointBuilder := &strings.Builder{} 26 | endpointBuilder.WriteString(bucket.Name) 27 | endpointBuilder.WriteString("." + bucket.BucketType) 28 | endpointBuilder.WriteString("." + bucket.Region) 29 | endpointBuilder.WriteString(".myqcloud.com") 30 | cosList.Append(&schema.Resource{ 31 | ID: d.id, 32 | Public: true, 33 | DNSName: endpointBuilder.String(), 34 | Provider: d.provider, 35 | }) 36 | } 37 | return cosList, nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/providers/qiniu/kodo.go: -------------------------------------------------------------------------------- 1 | package qiniu 2 | 3 | import ( 4 | "context" 5 | "github.com/projectdiscovery/gologger" 6 | "github.com/qiniu/go-sdk/v7/auth" 7 | "github.com/qiniu/go-sdk/v7/storage" 8 | "github.com/wgpsec/lc/pkg/schema" 9 | ) 10 | 11 | type kodoProvider struct { 12 | id string 13 | provider string 14 | kodoClient *auth.Credentials 15 | } 16 | 17 | func (d *kodoProvider) GetResource(ctx context.Context) (*schema.Resources, error) { 18 | var request storage.BucketV4Input 19 | var list = schema.NewResources() 20 | gologger.Debug().Msg("正在获取七牛云 Kodo 对象存储信息") 21 | cfg := storage.Config{ 22 | UseHTTPS: true, 23 | } 24 | bucketManager := storage.NewBucketManager(d.kodoClient, &cfg) 25 | for { 26 | response, err := bucketManager.BucketsV4(&request) 27 | if err != nil { 28 | return nil, err 29 | } 30 | for _, bucket := range response.Buckets { 31 | list.Append(&schema.Resource{ 32 | ID: d.id, 33 | Public: true, 34 | DNSName: bucket.Name, 35 | Provider: d.provider, 36 | }) 37 | } 38 | if response.IsTruncated { 39 | response.NextMarker = request.Marker 40 | } else { 41 | break 42 | } 43 | } 44 | return list, nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/providers/aliyun/domain.go: -------------------------------------------------------------------------------- 1 | package aliyun 2 | 3 | import ( 4 | "context" 5 | domain "github.com/alibabacloud-go/domain-20180129/v4/client" 6 | util "github.com/alibabacloud-go/tea-utils/v2/service" 7 | "github.com/alibabacloud-go/tea/tea" 8 | "github.com/projectdiscovery/gologger" 9 | "github.com/wgpsec/lc/pkg/schema" 10 | ) 11 | 12 | type domainProvider struct { 13 | id string 14 | provider string 15 | domainClient *domain.Client 16 | } 17 | 18 | func (d *domainProvider) GetResource(ctx context.Context) (*schema.Resources, error) { 19 | domainList := schema.NewResources() 20 | gologger.Debug().Msg("正在获取阿里云 Domain 资源信息") 21 | queryDomainListRequest := &domain.QueryDomainListRequest{ 22 | PageNum: tea.Int32(1), 23 | PageSize: tea.Int32(10), 24 | } 25 | runtime := &util.RuntimeOptions{} 26 | response, err := d.domainClient.QueryDomainListWithOptions(queryDomainListRequest, runtime) 27 | if err != nil { 28 | return nil, err 29 | } 30 | for _, domainResult := range response.Body.Data.Domain { 31 | domainList.Append(&schema.Resource{ 32 | ID: d.id, 33 | Public: true, 34 | DNSName: *domainResult.DomainName, 35 | Provider: d.provider, 36 | }) 37 | } 38 | return domainList, nil 39 | } 40 | -------------------------------------------------------------------------------- /pkg/providers/aliyun/oss.go: -------------------------------------------------------------------------------- 1 | package aliyun 2 | 3 | import ( 4 | "context" 5 | "github.com/aliyun/aliyun-oss-go-sdk/oss" 6 | "github.com/projectdiscovery/gologger" 7 | "github.com/wgpsec/lc/pkg/schema" 8 | "strings" 9 | ) 10 | 11 | type ossProvider struct { 12 | id string 13 | provider string 14 | ossClient *oss.Client 15 | } 16 | 17 | func (d *ossProvider) GetResource(ctx context.Context) (*schema.Resources, error) { 18 | ossList := schema.NewResources() 19 | marker := oss.Marker("") 20 | gologger.Debug().Msg("正在获取阿里云 OSS 资源信息") 21 | for { 22 | response, err := d.ossClient.ListBuckets(oss.MaxKeys(1000), marker) 23 | if err != nil { 24 | break 25 | } 26 | marker = oss.Marker(response.NextMarker) 27 | for _, bucket := range response.Buckets { 28 | endpointBuilder := &strings.Builder{} 29 | endpointBuilder.WriteString(bucket.Name) 30 | endpointBuilder.WriteString(".oss-" + bucket.Region) 31 | endpointBuilder.WriteString(".aliyuncs.com") 32 | ossList.Append(&schema.Resource{ 33 | ID: d.id, 34 | Public: true, 35 | DNSName: endpointBuilder.String(), 36 | Provider: d.provider, 37 | }) 38 | } 39 | if !response.IsTruncated { 40 | break 41 | } 42 | } 43 | return ossList, nil 44 | } 45 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: lc 2 | before: 3 | hooks: 4 | - go mod tidy 5 | - go generate ./... 6 | builds: 7 | - env: 8 | - CGO_ENABLED=0 9 | goos: 10 | - freebsd 11 | - darwin 12 | - windows 13 | - linux 14 | goarch: 15 | - 386 16 | - amd64 17 | - arm 18 | - arm64 19 | archives: 20 | - name_template: "{{ .ProjectName }}_{{ .Tag }}_{{ .Os }}_{{ .Arch }}" 21 | id: homebrew 22 | files: 23 | - README.md 24 | - CHANGELOG.md 25 | - LICENSE 26 | - static/* 27 | format_overrides: 28 | - goos: windows 29 | format: zip 30 | checksum: 31 | name_template: "checksums.txt" 32 | snapshot: 33 | name_template: "{{ incpatch .Version }}" 34 | changelog: 35 | sort: asc 36 | filters: 37 | exclude: 38 | - "^docs:" 39 | - "^doc:" 40 | - "^ci:" 41 | - "^Merge pull request" 42 | brews: 43 | - ids: 44 | - homebrew 45 | name: lc 46 | tap: 47 | owner: wgpsec 48 | name: homebrew-tap 49 | branch: master 50 | folder: Formula 51 | url_template: "https://github.com/wgpsec/lc/releases/download/{{ .Tag }}/{{ .ArtifactName }}" 52 | homepage: "https://wiki.teamssix.com/lc" 53 | description: "LC (List Cloud) is a multi-cloud attack surface asset enumeration tool." 54 | skip_upload: auto 55 | install: |- 56 | bin.install "lc" -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## 为 LC 贡献代码 2 | 3 | 首先很高兴你看到这个,非常欢迎您和我一起完善 LC,让我们一起编写一个好用的、有价值的、有意义的工具。 4 | 5 | ### 是否发现了 bug? 6 | 7 | 如果你发现了程序中的 bug,建议可以先在 [issue](https://github.com/wgpsec/lc/issues) 中进行搜索,看看以前有没有人提交过相同的 bug。 8 | 9 | ### 发现了 bug,并准备自己为 LC 打补丁 10 | 11 | 如果你发现了 bug,而且准备自己去修补它的话,则可以先把 LC Fork 到自己的仓库中,然后修复完后,提交 PR 到 LC 的 dev 分支下,另外记得在 PR 中详细说明 bug 的产生条件以及你是如何修复的,这可以帮助你更快的使这个 PR 通过。 12 | 13 | ### 为 LC 增加新功能 14 | 15 | 如果你发现 LC 现在的功能不能满足自己的需求,并打算自己去增加上这个功能,则可以先把 LC Fork 到自己的仓库中,然后编写完相应的功能后,提交 PR 到 LC 的 dev 分支下,另外记得在 PR 中详细说明增加的功能以及为什么要增加它,这可以帮助你更快的使这个 PR 通过。 16 | 17 | 建议师傅在增加新功能后,可以去完善对应的操作手册,操作手册项目地址:[github.com/teamssix/twiki](https://github.com/teamssix/twiki),先 fork 操作手册项目,然后在 [github.com/teamssix/twiki/tree/main/docs/lc](https://github.com/teamssix/twiki/tree/main/docs/lc) 目录下编辑 LC 操作手册的文档,最后提 pr 到 T Wiki 项目的 beta 分支下就行。 18 | 19 | ## 使你的 PR 更规范 20 | 21 | ### 规范 Git commit 22 | 23 | commit message 应尽可能的规范,为了保证和其他 commit 统一,建议 commit message 采用英文描述,并符合下面的格式: 24 | 25 | ```yaml 26 | type: subject 27 | ``` 28 | 29 | type 是 commit 的类别,subject 是对这个 commit 的英文描述,例如下面的示例: 30 | * feat: add object download function 31 | * perf: optimize the display of update progress bar 32 | 关于 Git commit 的更多规范要求可以参见这篇文章:[如何规范你的Git commit?](https://zhuanlan.zhihu.com/p/182553920) 33 | 34 | ### 规范代码格式 35 | 36 | 代码在编写完后,应使用 `go fmt` 和 `goimports`对代码进行格式化,从而使代码看起来更加整洁。 37 | 38 | 在 goland 中可以设置当代码保存时自动格式化代码,配置的步骤可以参见这篇文章:[GoLand:设置gofmt与goimports,保存时自动格式化代码](https://blog.csdn.net/qq_32907195/article/details/116755338) -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug 反馈 2 | description: 提交一个 issue 帮助改进这个项目 3 | title: "[Bug] 在这里输入你的标题" 4 | labels: ["bug"] 5 | assignees: 6 | - teamssix 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | 感谢您花时间提交这份 issue 12 | - type: textarea 13 | id: what-happened 14 | attributes: 15 | label: 描述你遇到的问题 16 | value: "详细描述你所遇到的问题" 17 | validations: 18 | required: true 19 | - type: textarea 20 | attributes: 21 | label: 复现步骤 22 | description: 复现这个问题的步骤 23 | placeholder: | 24 | 1. 在 xxx 情况下 25 | 2. 执行了 xxx 命令 26 | 3. 出现了 xxx 错误 27 | validations: 28 | required: true 29 | - type: dropdown 30 | id: system 31 | attributes: 32 | label: 操作系统 33 | description: 你在哪个操作系统下运行的 LC ? 34 | options: 35 | - MacOS 36 | - Linux 37 | - Windows 38 | validations: 39 | required: true 40 | - type: dropdown 41 | id: system-type 42 | attributes: 43 | label: 系统类型 44 | description: 你在哪个系统类型下运行的 LC ? 45 | options: 46 | - amd64 47 | - amd32 48 | - arm64 49 | - arm32 50 | validations: 51 | required: true 52 | - type: dropdown 53 | id: cf-version 54 | attributes: 55 | label: LC 版本 56 | description: 你运行的是 LC 的哪个版本? 57 | options: 58 | - 最新的 (Latest) 59 | - 0.0.1 60 | validations: 61 | required: true 62 | - type: textarea 63 | attributes: 64 | label: 补充信息 65 | description: | 66 | 链接?参考资料?任何可以给我们提供更多关于你所遇到的问题的背景资料的东西 67 | 68 | 提示:你可以通过点击这个区域来突出显示它,然后将文件拖入,从而附上图片或其他文件。 69 | validations: 70 | required: false 71 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/wgpsec/lc/pkg/schema" 6 | "gopkg.in/yaml.v3" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | type ErrNoSuchKey struct { 12 | Name string 13 | } 14 | 15 | // 文本处理 16 | 17 | func Contains(s []string, e string) bool { 18 | for _, a := range s { 19 | if strings.EqualFold(a, e) { 20 | return true 21 | } 22 | } 23 | return false 24 | } 25 | 26 | func (e *ErrNoSuchKey) Error() string { 27 | return fmt.Sprintf("no such key: %s", e.Name) 28 | } 29 | 30 | func DivideList(list []string, n int) [][]string { 31 | chunks := make([][]string, n) 32 | chunkSize := len(list) / n 33 | remaining := len(list) % n 34 | 35 | for i := 0; i < n; i++ { 36 | start := i * chunkSize 37 | end := start + chunkSize 38 | if i < remaining { 39 | end++ 40 | } 41 | if i == n-1 { 42 | end = len(list) 43 | } 44 | chunks[i] = list[start:end] 45 | } 46 | return chunks 47 | } 48 | 49 | func RemoveRepeatedElement(arr []string) (newArr []string) { 50 | newArr = make([]string, 0) 51 | for i := 0; i < len(arr); i++ { 52 | repeat := false 53 | for j := i + 1; j < len(arr); j++ { 54 | if arr[i] == arr[j] { 55 | repeat = true 56 | break 57 | } 58 | } 59 | if !repeat { 60 | newArr = append(newArr, arr[i]) 61 | } 62 | } 63 | return newArr 64 | } 65 | 66 | // 文件处理 67 | 68 | func ReadConfig(configFile string) (schema.Options, error) { 69 | var config schema.Options 70 | 71 | file, err := os.Open(configFile) 72 | if err != nil { 73 | return nil, err 74 | } 75 | defer file.Close() 76 | 77 | if err := yaml.NewDecoder(file).Decode(&config); err != nil { 78 | return nil, err 79 | } 80 | return config, nil 81 | } 82 | -------------------------------------------------------------------------------- /pkg/inventory/inventory.go: -------------------------------------------------------------------------------- 1 | package inventory 2 | 3 | import ( 4 | "fmt" 5 | "github.com/projectdiscovery/goflags" 6 | "github.com/wgpsec/lc/pkg/providers/aliyun" 7 | "github.com/wgpsec/lc/pkg/providers/baidu" 8 | "github.com/wgpsec/lc/pkg/providers/huawei" 9 | "github.com/wgpsec/lc/pkg/providers/liantong" 10 | "github.com/wgpsec/lc/pkg/providers/qiniu" 11 | "github.com/wgpsec/lc/pkg/providers/tencent" 12 | "github.com/wgpsec/lc/pkg/providers/tianyi" 13 | "github.com/wgpsec/lc/pkg/providers/yidong" 14 | "github.com/wgpsec/lc/pkg/schema" 15 | "github.com/wgpsec/lc/utils" 16 | ) 17 | 18 | type Inventory struct { 19 | Providers []schema.Provider 20 | } 21 | 22 | func New(options schema.Options, cs goflags.StringSlice) (*Inventory, error) { 23 | inventory := &Inventory{} 24 | 25 | for _, block := range options { 26 | value, ok := block.GetMetadata(utils.Provider) 27 | if !ok { 28 | continue 29 | } 30 | provider, err := nameToProvider(value, block, cs) 31 | if err != nil { 32 | return nil, err 33 | } 34 | inventory.Providers = append(inventory.Providers, provider) 35 | } 36 | return inventory, nil 37 | } 38 | 39 | func nameToProvider(value string, block schema.OptionBlock, cs goflags.StringSlice) (schema.Provider, error) { 40 | switch value { 41 | case utils.Aliyun: 42 | return aliyun.New(block, cs) 43 | case utils.Tencent: 44 | return tencent.New(block, cs) 45 | case utils.Huawei: 46 | return huawei.New(block, cs) 47 | case utils.TianYi: 48 | return tianyi.New(block, cs) 49 | case utils.Baidu: 50 | return baidu.New(block, cs) 51 | case utils.LianTong: 52 | return liantong.New(block, cs) 53 | case utils.QiNiu: 54 | return qiniu.New(block, cs) 55 | case utils.YiDong: 56 | return yidong.New(block, cs) 57 | default: 58 | return nil, fmt.Errorf("发现无效的云服务商名: %s", value) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pkg/providers/qiniu/qiniu.go: -------------------------------------------------------------------------------- 1 | package qiniu 2 | 3 | import ( 4 | "context" 5 | "github.com/projectdiscovery/goflags" 6 | "github.com/projectdiscovery/gologger" 7 | "github.com/qiniu/go-sdk/v7/auth" 8 | "github.com/wgpsec/lc/pkg/schema" 9 | "github.com/wgpsec/lc/utils" 10 | "strings" 11 | ) 12 | 13 | type Provider struct { 14 | id string 15 | provider string 16 | kodoClient *auth.Credentials 17 | cloudServices []string 18 | } 19 | 20 | func New(options schema.OptionBlock, cs goflags.StringSlice) (*Provider, error) { 21 | var ( 22 | kodoClient *auth.Credentials 23 | cloudServices []string 24 | ) 25 | accessKeyID, ok := options.GetMetadata(utils.AccessKey) 26 | if !ok { 27 | return nil, &utils.ErrNoSuchKey{Name: utils.AccessKey} 28 | } 29 | accessKeySecret, ok := options.GetMetadata(utils.SecretKey) 30 | if !ok { 31 | return nil, &utils.ErrNoSuchKey{Name: utils.SecretKey} 32 | } 33 | id, _ := options.GetMetadata(utils.Id) 34 | 35 | gologger.Debug().Msg("找到七牛云访问永久访问凭证") 36 | 37 | if cs[0] == "all" { 38 | cloudServicesResult, _ := options.GetMetadata(utils.CloudServices) 39 | cloudServices = strings.Split(cloudServicesResult, ",") 40 | } else { 41 | cloudServices = cs 42 | } 43 | for _, cloudService := range cloudServices { 44 | switch cloudService { 45 | case "kodo": 46 | // kodo client 47 | kodoClient = auth.New(accessKeyID, accessKeySecret) 48 | } 49 | } 50 | return &Provider{provider: utils.QiNiu, id: id, kodoClient: kodoClient, cloudServices: cloudServices}, nil 51 | } 52 | 53 | func (p *Provider) Resources(ctx context.Context, cs goflags.StringSlice) (*schema.Resources, error) { 54 | finalList := schema.NewResources() 55 | for _, cloudService := range p.cloudServices { 56 | switch cloudService { 57 | case "kodo": 58 | kodoProvider := &kodoProvider{kodoClient: p.kodoClient, id: p.id, provider: p.provider} 59 | buckets, err := kodoProvider.GetResource(ctx) 60 | if err != nil { 61 | return nil, err 62 | } 63 | gologger.Info().Msgf("获取到 %d 条七牛云 Kodo 对象存储信息", len(buckets.GetItems())) 64 | finalList.Merge(buckets) 65 | } 66 | } 67 | return finalList, nil 68 | } 69 | 70 | func (p *Provider) Name() string { 71 | return p.provider 72 | } 73 | func (p *Provider) ID() string { 74 | return p.id 75 | } 76 | -------------------------------------------------------------------------------- /cmd/configFile.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | const defaultConfigFile = `# # lc (list cloud) 的云服务商配置文件 4 | 5 | # # 配置文件说明 6 | 7 | # # provider 是云服务商的名字 8 | # - provider: provider_name 9 | # # id 是当前配置文件的名字 10 | # id: test 11 | # # access_key 是这个云的访问凭证 Key 部分 12 | # access_key: 13 | # # secret_key 是这个云的访问凭证 Secret 部分 14 | # secret_key: 15 | # # (可选)session_token 是这个云的访问凭证 session token 部分,仅在访问凭证是临时访问配置时才需要填写这部分的内容 16 | # session_token: 17 | 18 | # # 阿里云 19 | # # 访问凭证获取地址:https://ram.console.aliyun.com 20 | # - provider: aliyun 21 | # id: aliyun_default 22 | # cloud_services: ecs,oss,rds,fc,domain 23 | # access_key: 24 | # secret_key: 25 | # session_token: 26 | 27 | # # 腾讯云 28 | # # 访问凭证获取地址:https://console.cloud.tencent.com/cam 29 | # - provider: tencent 30 | # id: tencent_cloud_default 31 | # cloud_services: cvm,lh,cos 32 | # access_key: 33 | # secret_key: 34 | # session_token: 35 | 36 | # # 华为云 37 | # # 访问凭证获取地址:https://console.huaweicloud.com/iam 38 | # - provider: huawei 39 | # id: huawei_cloud_default 40 | # cloud_services: obs 41 | # access_key: 42 | # secret_key: 43 | # session_token: 44 | 45 | # # 天翼云 46 | # # 访问凭证获取地址:https://oos-cn.ctyun.cn/oos/ctyun/iam/dist/index.html#/certificate 47 | # - provider: tianyi 48 | # id: tianyi_cloud_default 49 | # cloud_services: oos 50 | # access_key: 51 | # secret_key: 52 | 53 | # # 百度云 54 | # # 访问凭证获取地址:https://console.bce.baidu.com/iam/ 55 | # - provider: baidu 56 | # id: baidu_cloud_default 57 | # cloud_services: bos,bcc 58 | # access_key: 59 | # secret_key: 60 | # session_token: 61 | 62 | # # 联通云 63 | # # 访问凭证获取地址:https://console.cucloud.cn/console/uiam 64 | # - provider: liantong 65 | # id: liantong_cloud_default 66 | # cloud_services: oss 67 | # access_key: 68 | # secret_key: 69 | # session_token: 70 | 71 | # # 七牛云 72 | # # 访问凭证获取地址:https://portal.qiniu.com/developer/user/key 73 | # - provider: qiniu 74 | # id: qiniu_cloud_default 75 | # cloud_services: kodo 76 | # access_key: 77 | # secret_key: 78 | 79 | # # 移动云 80 | # # 访问凭证获取地址:https://console.ecloud.10086.cn/api/page/eos-console-web/CIDC-RP-00/eos/key 81 | # - provider: yidong 82 | # id: yidong_cloud_default 83 | # cloud_services: eos 84 | # access_key: 85 | # secret_key: 86 | # session_token: 87 | ` 88 | -------------------------------------------------------------------------------- /pkg/providers/yidong/yidong.go: -------------------------------------------------------------------------------- 1 | package yidong 2 | 3 | import ( 4 | "context" 5 | "github.com/projectdiscovery/goflags" 6 | "github.com/projectdiscovery/gologger" 7 | "github.com/wgpsec/lc/pkg/schema" 8 | "github.com/wgpsec/lc/utils" 9 | "strings" 10 | ) 11 | 12 | type Provider struct { 13 | id string 14 | provider string 15 | config providerConfig 16 | cloudServices []string 17 | } 18 | 19 | type providerConfig struct { 20 | accessKeyID string 21 | accessKeySecret string 22 | sessionToken string 23 | } 24 | 25 | func New(options schema.OptionBlock, cs goflags.StringSlice) (*Provider, error) { 26 | var cloudServices []string 27 | 28 | accessKeyID, ok := options.GetMetadata(utils.AccessKey) 29 | if !ok { 30 | return nil, &utils.ErrNoSuchKey{Name: utils.AccessKey} 31 | } 32 | accessKeySecret, ok := options.GetMetadata(utils.SecretKey) 33 | if !ok { 34 | return nil, &utils.ErrNoSuchKey{Name: utils.SecretKey} 35 | } 36 | id, _ := options.GetMetadata(utils.Id) 37 | sessionToken, stsOk := options.GetMetadata(utils.SessionToken) 38 | 39 | if stsOk { 40 | gologger.Debug().Msg("找到移动云临时访问凭证") 41 | } else { 42 | gologger.Debug().Msg("找到移动云永久访问凭证") 43 | } 44 | if cs[0] == "all" { 45 | cloudServicesResult, _ := options.GetMetadata(utils.CloudServices) 46 | cloudServices = strings.Split(cloudServicesResult, ",") 47 | } else { 48 | cloudServices = cs 49 | } 50 | config := providerConfig{ 51 | accessKeyID: accessKeyID, 52 | accessKeySecret: accessKeySecret, 53 | sessionToken: sessionToken, 54 | } 55 | return &Provider{id: id, provider: utils.YiDong, config: config, cloudServices: cloudServices}, nil 56 | } 57 | 58 | func (p *Provider) Name() string { 59 | return p.provider 60 | } 61 | 62 | func (p *Provider) ID() string { 63 | return p.id 64 | } 65 | 66 | func (p *Provider) Resources(ctx context.Context, cs goflags.StringSlice) (*schema.Resources, error) { 67 | finalList := schema.NewResources() 68 | for _, cloudService := range p.cloudServices { 69 | switch cloudService { 70 | case "eos": 71 | eosProvider := &eosProvider{config: p.config, id: p.id, provider: p.provider} 72 | buckets, err := eosProvider.GetResource(ctx) 73 | if err != nil { 74 | return nil, err 75 | } 76 | gologger.Info().Msgf("获取到 %d 条移动云 EOS 信息", len(buckets.GetItems())) 77 | finalList.Merge(buckets) 78 | } 79 | } 80 | return finalList, nil 81 | } 82 | -------------------------------------------------------------------------------- /pkg/providers/liantong/liantong.go: -------------------------------------------------------------------------------- 1 | package liantong 2 | 3 | import ( 4 | "context" 5 | "github.com/projectdiscovery/goflags" 6 | "github.com/projectdiscovery/gologger" 7 | "github.com/wgpsec/lc/pkg/schema" 8 | "github.com/wgpsec/lc/utils" 9 | "strings" 10 | ) 11 | 12 | type Provider struct { 13 | id string 14 | provider string 15 | config providerConfig 16 | cloudServices []string 17 | } 18 | 19 | type providerConfig struct { 20 | accessKeyID string 21 | accessKeySecret string 22 | sessionToken string 23 | cloudServices []string 24 | } 25 | 26 | func New(options schema.OptionBlock, cs goflags.StringSlice) (*Provider, error) { 27 | var cloudServices []string 28 | accessKeyID, ok := options.GetMetadata(utils.AccessKey) 29 | if !ok { 30 | return nil, &utils.ErrNoSuchKey{Name: utils.AccessKey} 31 | } 32 | accessKeySecret, ok := options.GetMetadata(utils.SecretKey) 33 | if !ok { 34 | return nil, &utils.ErrNoSuchKey{Name: utils.SecretKey} 35 | } 36 | id, _ := options.GetMetadata(utils.Id) 37 | sessionToken, stsOk := options.GetMetadata(utils.SessionToken) 38 | if stsOk { 39 | gologger.Debug().Msg("找到联通云临时访问凭证") 40 | } else { 41 | gologger.Debug().Msg("找到联通云永久访问凭证") 42 | } 43 | if cs[0] == "all" { 44 | cloudServicesResult, _ := options.GetMetadata(utils.CloudServices) 45 | cloudServices = strings.Split(cloudServicesResult, ",") 46 | } else { 47 | cloudServices = cs 48 | } 49 | config := providerConfig{ 50 | accessKeyID: accessKeyID, 51 | accessKeySecret: accessKeySecret, 52 | sessionToken: sessionToken, 53 | } 54 | return &Provider{id: id, provider: utils.LianTong, config: config, cloudServices: cloudServices}, nil 55 | } 56 | 57 | func (p *Provider) Name() string { 58 | return p.provider 59 | } 60 | 61 | func (p *Provider) ID() string { 62 | return p.id 63 | } 64 | 65 | func (p *Provider) Resources(ctx context.Context, cs goflags.StringSlice) (*schema.Resources, error) { 66 | finalList := schema.NewResources() 67 | for _, cloudService := range p.cloudServices { 68 | switch cloudService { 69 | case "oss": 70 | ossProvider := &ossProvider{config: p.config, id: p.id, provider: p.provider} 71 | buckets, err := ossProvider.GetResource(ctx) 72 | if err != nil { 73 | return nil, err 74 | } 75 | gologger.Info().Msgf("获取到 %d 条联通云 OSS 信息", len(buckets.GetItems())) 76 | finalList.Merge(buckets) 77 | } 78 | } 79 | return finalList, nil 80 | } 81 | -------------------------------------------------------------------------------- /pkg/providers/tianyi/tianyi.go: -------------------------------------------------------------------------------- 1 | package tianyi 2 | 3 | import ( 4 | "context" 5 | "github.com/projectdiscovery/goflags" 6 | "github.com/projectdiscovery/gologger" 7 | "github.com/teamssix/oos-go-sdk/oos" 8 | "github.com/wgpsec/lc/pkg/schema" 9 | "github.com/wgpsec/lc/utils" 10 | "strings" 11 | ) 12 | 13 | type Provider struct { 14 | id string 15 | provider string 16 | oosClient *oos.Client 17 | cloudServices []string 18 | } 19 | 20 | func New(options schema.OptionBlock, cs goflags.StringSlice) (*Provider, error) { 21 | var ( 22 | err error 23 | oosClient *oos.Client 24 | cloudServices []string 25 | ) 26 | accessKeyID, ok := options.GetMetadata(utils.AccessKey) 27 | if !ok { 28 | return nil, &utils.ErrNoSuchKey{Name: utils.AccessKey} 29 | } 30 | accessKeySecret, ok := options.GetMetadata(utils.SecretKey) 31 | if !ok { 32 | return nil, &utils.ErrNoSuchKey{Name: utils.SecretKey} 33 | } 34 | id, _ := options.GetMetadata(utils.Id) 35 | 36 | gologger.Debug().Msg("找到天翼云访问永久访问凭证") 37 | if cs[0] == "all" { 38 | cloudServicesResult, _ := options.GetMetadata(utils.CloudServices) 39 | cloudServices = strings.Split(cloudServicesResult, ",") 40 | } else { 41 | cloudServices = cs 42 | } 43 | for _, cloudService := range cloudServices { 44 | switch cloudService { 45 | case "oos": 46 | // oos client 47 | clientOptionV4 := oos.V4Signature(true) 48 | isEnableSha256 := oos.EnableSha256ForPayload(true) 49 | oosClient, err = oos.New("https://oos-cn.ctyunapi.cn", accessKeyID, accessKeySecret, clientOptionV4, isEnableSha256) 50 | if err != nil { 51 | return nil, err 52 | } 53 | } 54 | } 55 | return &Provider{provider: utils.TianYi, id: id, oosClient: oosClient, cloudServices: cloudServices}, nil 56 | } 57 | 58 | func (p *Provider) Resources(ctx context.Context, cs goflags.StringSlice) (*schema.Resources, error) { 59 | finalList := schema.NewResources() 60 | for _, cloudService := range p.cloudServices { 61 | switch cloudService { 62 | case "oos": 63 | oosProvider := &oosProvider{oosClient: p.oosClient, id: p.id, provider: p.provider} 64 | buckets, err := oosProvider.GetResource(ctx) 65 | if err != nil { 66 | return nil, err 67 | } 68 | gologger.Info().Msgf("获取到 %d 条天翼云 OOS 对象存储信息", len(buckets.GetItems())) 69 | finalList.Merge(buckets) 70 | } 71 | } 72 | return finalList, nil 73 | } 74 | 75 | func (p *Provider) Name() string { 76 | return p.provider 77 | } 78 | func (p *Provider) ID() string { 79 | return p.id 80 | } 81 | -------------------------------------------------------------------------------- /pkg/providers/tencent/lh.go: -------------------------------------------------------------------------------- 1 | package tencent 2 | 3 | import ( 4 | "context" 5 | "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" 6 | "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" 7 | lh "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/lighthouse/v20200324" 8 | "github.com/wgpsec/lc/pkg/schema" 9 | "sync" 10 | ) 11 | 12 | var lhList = schema.NewResources() 13 | 14 | func (d *instanceProvider) GetLHResource(ctx context.Context) (*schema.Resources, error) { 15 | var ( 16 | threads int 17 | err error 18 | wg sync.WaitGroup 19 | regions []string 20 | ) 21 | threads = schema.GetThreads() 22 | 23 | for _, region := range d.lhRegions { 24 | regions = append(regions, *region.Region) 25 | } 26 | taskCh := make(chan string, threads) 27 | for i := 0; i < threads; i++ { 28 | wg.Add(1) 29 | go func() { 30 | err = d.describeLHInstances(taskCh, &wg) 31 | if err != nil { 32 | return 33 | } 34 | }() 35 | } 36 | for _, item := range regions { 37 | taskCh <- item 38 | } 39 | close(taskCh) 40 | wg.Wait() 41 | return lhList, nil 42 | } 43 | 44 | func (d *instanceProvider) describeLHInstances(ch <-chan string, wg *sync.WaitGroup) error { 45 | defer wg.Done() 46 | var ( 47 | err error 48 | lhClient *lh.Client 49 | response *lh.DescribeInstancesResponse 50 | ) 51 | for region := range ch { 52 | cpf := profile.NewClientProfile() 53 | cpf.HttpProfile.Endpoint = "lighthouse.tencentcloudapi.com" 54 | lhClient, err = lh.NewClient(d.credential, region, cpf) 55 | if err != nil { 56 | continue 57 | } 58 | request := lh.NewDescribeInstancesRequest() 59 | request.Limit = common.Int64Ptr(100) 60 | request.SetScheme("https") 61 | response, err = lhClient.DescribeInstances(request) 62 | if err != nil { 63 | continue 64 | } 65 | for _, instance := range response.Response.InstanceSet { 66 | var ( 67 | ipv4 []string 68 | privateIPv4 string 69 | ) 70 | 71 | if len(instance.PublicAddresses) > 0 { 72 | for _, v := range instance.PublicAddresses { 73 | ipv4 = append(ipv4, *v) 74 | } 75 | } 76 | if len(instance.PrivateAddresses) > 0 { 77 | privateIPv4 = *instance.PrivateAddresses[0] 78 | } 79 | for _, v := range ipv4 { 80 | lhList.Append(&schema.Resource{ 81 | ID: d.id, 82 | Provider: d.provider, 83 | PublicIPv4: v, 84 | PrivateIpv4: privateIPv4, 85 | Public: v != "", 86 | }) 87 | } 88 | } 89 | } 90 | return err 91 | } 92 | -------------------------------------------------------------------------------- /pkg/providers/huawei/huawei.go: -------------------------------------------------------------------------------- 1 | package huawei 2 | 3 | import ( 4 | "context" 5 | "github.com/huaweicloud/huaweicloud-sdk-go-obs/obs" 6 | "github.com/projectdiscovery/goflags" 7 | "github.com/projectdiscovery/gologger" 8 | "github.com/wgpsec/lc/pkg/schema" 9 | "github.com/wgpsec/lc/utils" 10 | "strings" 11 | ) 12 | 13 | type Provider struct { 14 | id string 15 | provider string 16 | obsClient *obs.ObsClient 17 | cloudServices []string 18 | } 19 | 20 | func New(options schema.OptionBlock, cs goflags.StringSlice) (*Provider, error) { 21 | var ( 22 | region = "cn-north-4" 23 | err error 24 | obsClient *obs.ObsClient 25 | cloudServices []string 26 | ) 27 | accessKeyID, ok := options.GetMetadata(utils.AccessKey) 28 | if !ok { 29 | return nil, &utils.ErrNoSuchKey{Name: utils.AccessKey} 30 | } 31 | accessKeySecret, ok := options.GetMetadata(utils.SecretKey) 32 | if !ok { 33 | return nil, &utils.ErrNoSuchKey{Name: utils.SecretKey} 34 | } 35 | id, _ := options.GetMetadata(utils.Id) 36 | sessionToken, okST := options.GetMetadata(utils.SessionToken) 37 | 38 | if okST { 39 | gologger.Debug().Msg("找到华为云访问临时访问凭证") 40 | } else { 41 | gologger.Debug().Msg("找到华为云访问永久访问凭证") 42 | } 43 | if cs[0] == "all" { 44 | cloudServicesResult, _ := options.GetMetadata(utils.CloudServices) 45 | cloudServices = strings.Split(cloudServicesResult, ",") 46 | } else { 47 | cloudServices = cs 48 | } 49 | for _, cloudService := range cloudServices { 50 | switch cloudService { 51 | case "obs": 52 | // obs client 53 | if okST { 54 | obsClient, err = obs.New(accessKeyID, accessKeySecret, "https://obs."+region+".myhuaweicloud.com", obs.WithSecurityToken(sessionToken)) 55 | } else { 56 | obsClient, err = obs.New(accessKeyID, accessKeySecret, "https://obs."+region+".myhuaweicloud.com") 57 | } 58 | if err != nil { 59 | return nil, err 60 | } 61 | } 62 | } 63 | 64 | return &Provider{provider: utils.Huawei, id: id, obsClient: obsClient, cloudServices: cloudServices}, nil 65 | } 66 | 67 | func (p *Provider) Resources(ctx context.Context, cs goflags.StringSlice) (*schema.Resources, error) { 68 | finalList := schema.NewResources() 69 | for _, cloudService := range p.cloudServices { 70 | switch cloudService { 71 | case "obs": 72 | obsProvider := &obsProvider{obsClient: p.obsClient, id: p.id, provider: p.provider} 73 | buckets, err := obsProvider.GetResource(ctx) 74 | if err != nil { 75 | return nil, err 76 | } 77 | gologger.Info().Msgf("获取到 %d 条华为云 OBS 信息", len(buckets.GetItems())) 78 | finalList.Merge(buckets) 79 | } 80 | } 81 | return finalList, nil 82 | } 83 | 84 | func (p *Provider) Name() string { 85 | return p.provider 86 | } 87 | func (p *Provider) ID() string { 88 | return p.id 89 | } 90 | -------------------------------------------------------------------------------- /pkg/providers/tencent/cvm.go: -------------------------------------------------------------------------------- 1 | package tencent 2 | 3 | import ( 4 | "context" 5 | "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" 6 | "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" 7 | cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312" 8 | lh "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/lighthouse/v20200324" 9 | "github.com/wgpsec/lc/pkg/schema" 10 | "sync" 11 | ) 12 | 13 | type instanceProvider struct { 14 | id string 15 | provider string 16 | credential *common.Credential 17 | cvmRegions []*cvm.RegionInfo 18 | lhRegions []*lh.RegionInfo 19 | } 20 | 21 | var cvmList = schema.NewResources() 22 | 23 | func (d *instanceProvider) GetCVMResource(ctx context.Context) (*schema.Resources, error) { 24 | var ( 25 | threads int 26 | //err error 27 | wg sync.WaitGroup 28 | regions []string 29 | ) 30 | threads = schema.GetThreads() 31 | 32 | for _, region := range d.cvmRegions { 33 | regions = append(regions, *region.Region) 34 | } 35 | 36 | taskCh := make(chan string, threads) 37 | for i := 0; i < threads; i++ { 38 | wg.Add(1) 39 | go func() { 40 | d.describeCVMInstances(taskCh, &wg) 41 | //if err != nil { 42 | // return 43 | //} 44 | }() 45 | } 46 | for _, item := range regions { 47 | taskCh <- item 48 | } 49 | close(taskCh) 50 | wg.Wait() 51 | return cvmList, nil 52 | } 53 | 54 | func (d *instanceProvider) describeCVMInstances(ch <-chan string, wg *sync.WaitGroup) error { 55 | defer wg.Done() 56 | var ( 57 | err error 58 | cvmClient *cvm.Client 59 | response *cvm.DescribeInstancesResponse 60 | ) 61 | for region := range ch { 62 | cpf := profile.NewClientProfile() 63 | cpf.HttpProfile.Endpoint = "cvm.tencentcloudapi.com" 64 | cvmClient, err = cvm.NewClient(d.credential, region, cpf) 65 | if err != nil { 66 | continue 67 | } 68 | request := cvm.NewDescribeInstancesRequest() 69 | request.Limit = common.Int64Ptr(100) 70 | request.SetScheme("https") 71 | response, err = cvmClient.DescribeInstances(request) 72 | if err != nil { 73 | continue 74 | } 75 | for _, instance := range response.Response.InstanceSet { 76 | var ( 77 | ipv4 []string 78 | privateIPv4 string 79 | ) 80 | 81 | if len(instance.PublicIpAddresses) > 0 { 82 | for _, v := range instance.PublicIpAddresses { 83 | ipv4 = append(ipv4, *v) 84 | } 85 | } 86 | if len(instance.PrivateIpAddresses) > 0 { 87 | privateIPv4 = *instance.PrivateIpAddresses[0] 88 | } 89 | for _, v := range ipv4 { 90 | cvmList.Append(&schema.Resource{ 91 | ID: d.id, 92 | Provider: d.provider, 93 | PublicIPv4: v, 94 | PrivateIpv4: privateIPv4, 95 | Public: v != "", 96 | }) 97 | } 98 | } 99 | } 100 | return err 101 | } 102 | -------------------------------------------------------------------------------- /pkg/providers/aliyun/fc3.go: -------------------------------------------------------------------------------- 1 | package aliyun 2 | 3 | import ( 4 | "fmt" 5 | openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" 6 | fc "github.com/alibabacloud-go/fc-20230330/v4/client" 7 | "github.com/aliyun/alibaba-cloud-sdk-go/services/sts" 8 | "github.com/projectdiscovery/gologger" 9 | "github.com/wgpsec/lc/pkg/schema" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | type function3Provider struct { 15 | id string 16 | identity *sts.GetCallerIdentityResponse 17 | provider string 18 | config providerConfig 19 | fcRegions []FcRegion 20 | } 21 | 22 | var fc3List = schema.NewResources() 23 | 24 | func (f *function3Provider) GetResource() (*schema.Resources, error) { 25 | var ( 26 | threads int 27 | err error 28 | wg sync.WaitGroup 29 | regions []string 30 | ) 31 | 32 | for _, region := range f.fcRegions { 33 | if !strings.Contains(region.RegionId, "finance") { 34 | regions = append(regions, region.RegionId) 35 | } 36 | } 37 | 38 | threads = schema.GetThreads() 39 | taskCh := make(chan string, threads) 40 | for i := 0; i < threads; i++ { 41 | wg.Add(1) 42 | go func() { 43 | err = f.listCustomDomains(taskCh, &wg) 44 | if err != nil { 45 | return 46 | } 47 | }() 48 | } 49 | for _, item := range regions { 50 | taskCh <- item 51 | } 52 | close(taskCh) 53 | wg.Wait() 54 | 55 | return fc3List, nil 56 | } 57 | 58 | func (f *function3Provider) newFcConfig(region string) *openapi.Config { 59 | endpoint := fmt.Sprintf("%s.%s.fc.aliyuncs.com", f.identity.AccountId, region) 60 | return &openapi.Config{ 61 | AccessKeyId: &f.config.accessKeyID, 62 | AccessKeySecret: &f.config.accessKeySecret, 63 | SecurityToken: &f.config.sessionToken, 64 | Endpoint: &endpoint, 65 | RegionId: ®ion, 66 | } 67 | } 68 | 69 | func (f *function3Provider) listCustomDomains(ch <-chan string, wg *sync.WaitGroup) error { 70 | defer wg.Done() 71 | var ( 72 | err error 73 | fcClient *fc.Client 74 | domainRes *fc.ListCustomDomainsResponse 75 | ) 76 | 77 | for region := range ch { 78 | gologger.Debug().Msgf("正在获取 %s 区域下的阿里云 FC 3.0 自定义域名资源信息", region) 79 | fcConfig := f.newFcConfig(region) 80 | fcClient, err = fc.NewClient(fcConfig) 81 | if err != nil { 82 | gologger.Debug().Msgf("%s endpoint NewClient err: %s", *fcConfig.Endpoint, err) 83 | break 84 | } 85 | 86 | lcdReq := &fc.ListCustomDomainsRequest{} 87 | domainRes, err = fcClient.ListCustomDomains(lcdReq) 88 | if err != nil { 89 | gologger.Debug().Msgf("%s endpoint ListCustomDomains err: %s", *fcClient.Endpoint, err) 90 | continue 91 | } 92 | for _, cd := range domainRes.Body.CustomDomains { 93 | fc3List.Append(&schema.Resource{ 94 | ID: f.id, 95 | Provider: f.provider, 96 | DNSName: fmt.Sprintf("%s://%s", strings.ToLower(*cd.Protocol), *cd.DomainName), 97 | }) 98 | } 99 | } 100 | return err 101 | } 102 | -------------------------------------------------------------------------------- /pkg/providers/baidu/bcc.go: -------------------------------------------------------------------------------- 1 | package baidu 2 | 3 | import ( 4 | "context" 5 | "github.com/baidubce/bce-sdk-go/auth" 6 | "github.com/baidubce/bce-sdk-go/services/bcc" 7 | "github.com/baidubce/bce-sdk-go/services/bcc/api" 8 | "github.com/wgpsec/lc/pkg/schema" 9 | "sync" 10 | ) 11 | 12 | type instanceProvider struct { 13 | id string 14 | provider string 15 | config providerConfig 16 | } 17 | 18 | var list = schema.NewResources() 19 | 20 | func (d *instanceProvider) GetResource(ctx context.Context) (*schema.Resources, error) { 21 | var ( 22 | threads int 23 | err error 24 | wg sync.WaitGroup 25 | ) 26 | var endpoints = []string{ 27 | "https://bcc.bj.baidubce.com", 28 | "https://bcc.gz.baidubce.com", 29 | "https://bcc.su.baidubce.com", 30 | "https://bcc.hkg.baidubce.com", 31 | "https://bcc.fwh.baidubce.com", 32 | "https://bcc.bd.baidubce.com", 33 | "https://bcc.cd.baidubce.com", 34 | "https://bcc.nj.baidubce.com", 35 | "https://bcc.fsh.baidubce.com", 36 | } 37 | threads = schema.GetThreads() 38 | 39 | taskCh := make(chan string, threads) 40 | for i := 0; i < threads; i++ { 41 | wg.Add(1) 42 | go func() { 43 | err = d.describeInstances(taskCh, &wg) 44 | if err != nil { 45 | return 46 | } 47 | }() 48 | } 49 | for _, item := range endpoints { 50 | taskCh <- item 51 | } 52 | close(taskCh) 53 | wg.Wait() 54 | return list, nil 55 | } 56 | 57 | func (d *instanceProvider) describeInstances(ch <-chan string, wg *sync.WaitGroup) error { 58 | defer wg.Done() 59 | var ( 60 | err error 61 | bccClient *bcc.Client 62 | ) 63 | for endpoint := range ch { 64 | if d.config.okST { 65 | bccClient, err = bcc.NewClient(d.config.accessKeyID, d.config.accessKeySecret, "") 66 | if err != nil { 67 | continue 68 | } 69 | stsCredential, err := auth.NewSessionBceCredentials( 70 | d.config.accessKeyID, 71 | d.config.accessKeySecret, 72 | d.config.sessionToken) 73 | if err != nil { 74 | continue 75 | } 76 | bccClient.Config.Credentials = stsCredential 77 | } else { 78 | bccClient, err = bcc.NewClient(d.config.accessKeyID, d.config.accessKeySecret, endpoint) 79 | if err != nil { 80 | continue 81 | } 82 | } 83 | listArgs := &api.ListInstanceArgs{} 84 | for { 85 | response, err := bccClient.ListInstances(listArgs) 86 | if err != nil { 87 | break 88 | } 89 | for _, instance := range response.Instances { 90 | var ( 91 | ipv4 string 92 | privateIPv4 string 93 | ) 94 | ipv4 = instance.PublicIP 95 | privateIPv4 = instance.InternalIP 96 | list.Append(&schema.Resource{ 97 | ID: d.id, 98 | Provider: d.provider, 99 | PublicIPv4: ipv4, 100 | PrivateIpv4: privateIPv4, 101 | Public: ipv4 != "", 102 | }) 103 | } 104 | if response.NextMarker == "" { 105 | break 106 | } 107 | listArgs.Marker = response.NextMarker 108 | } 109 | } 110 | return err 111 | } 112 | -------------------------------------------------------------------------------- /pkg/providers/liantong/oss.go: -------------------------------------------------------------------------------- 1 | package liantong 2 | 3 | import ( 4 | "context" 5 | "github.com/aws/aws-sdk-go/aws" 6 | "github.com/aws/aws-sdk-go/aws/credentials" 7 | "github.com/aws/aws-sdk-go/aws/session" 8 | "github.com/aws/aws-sdk-go/service/s3" 9 | "github.com/wgpsec/lc/pkg/schema" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | type ossProvider struct { 15 | id string 16 | provider string 17 | config providerConfig 18 | } 19 | 20 | type regions struct { 21 | region string 22 | endpoint string 23 | } 24 | 25 | var list = schema.NewResources() 26 | 27 | func (d *ossProvider) GetResource(ctx context.Context) (*schema.Resources, error) { 28 | var ( 29 | threads int 30 | err error 31 | wg sync.WaitGroup 32 | ) 33 | 34 | zones := []regions{ 35 | {region: "cn-langfang-2", endpoint: "obs-helf.cucloud.cn"}, 36 | {region: "cn-xiamen-1", endpoint: "obs-fjxm.cucloud.cn"}, 37 | {region: "cn-nanping-1", endpoint: "obs-fjnp.cucloud.cn"}, 38 | {region: "cn-ningde-1", endpoint: "obs-fjnd.cucloud.cn"}, 39 | {region: "cn-huhehaote-2", endpoint: "obs-nmhhht2.cucloud.cn"}, 40 | {region: "cn-guiyang-2", endpoint: "obs-gzgy2.cucloud.cn"}, 41 | {region: "cn-chongqing-1", endpoint: "obs-cq.cucloud.cn"}, 42 | {region: "cn-shenzhen-1", endpoint: "obs-gdsz.cucloud.cn"}, 43 | {region: "cn-shengyang-1", endpoint: "obs-lnsy.cucloud.cn"}, 44 | {region: "cn-harbin-1", endpoint: "obs-hlhrb.cucloud.cn"}, 45 | {region: "cn-shanghai-1", endpoint: "obs-sh.cucloud.cn"}, 46 | //{region: "cn-huhehaote-3", endpoint: "obs-nmhhht3.cucloud.cn"}, 47 | //{region: "cn-shijiazhuang-1", endpoint: "obs-hesjz.cucloud.cn"}, 48 | {region: "cn-changsha-1", endpoint: "obs-hncs.cucloud.cn"}, 49 | } 50 | threads = schema.GetThreads() 51 | 52 | taskCh := make(chan regions, threads) 53 | for i := 0; i < threads; i++ { 54 | wg.Add(1) 55 | go func() { 56 | err = d.listBuckets(taskCh, &wg) 57 | if err != nil { 58 | return 59 | } 60 | }() 61 | } 62 | for _, item := range zones { 63 | taskCh <- item 64 | } 65 | close(taskCh) 66 | wg.Wait() 67 | return list, nil 68 | 69 | } 70 | 71 | func (d *ossProvider) listBuckets(ch <-chan regions, wg *sync.WaitGroup) error { 72 | defer wg.Done() 73 | var err error 74 | for region := range ch { 75 | config := aws.NewConfig() 76 | config.WithRegion(region.region) 77 | config.WithEndpoint("https://" + region.endpoint) 78 | config.WithCredentials(credentials.NewStaticCredentials(d.config.accessKeyID, d.config.accessKeySecret, d.config.sessionToken)) 79 | session, err := session.NewSession(config) 80 | 81 | if err != nil { 82 | continue 83 | } 84 | s3Client := s3.New(session) 85 | 86 | listBucketsOutput, err := s3Client.ListBuckets(nil) 87 | if err != nil { 88 | continue 89 | } 90 | for _, bucket := range listBucketsOutput.Buckets { 91 | endpointBuilder := &strings.Builder{} 92 | endpointBuilder.WriteString(aws.StringValue(bucket.Name)) 93 | endpointBuilder.WriteString("." + region.endpoint) 94 | list.Append(&schema.Resource{ 95 | ID: d.id, 96 | Public: true, 97 | DNSName: endpointBuilder.String(), 98 | Provider: d.provider, 99 | }) 100 | } 101 | } 102 | return err 103 | } 104 | -------------------------------------------------------------------------------- /cmd/runner.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "github.com/projectdiscovery/gologger" 8 | "github.com/wgpsec/lc/pkg/inventory" 9 | "github.com/wgpsec/lc/pkg/schema" 10 | "github.com/wgpsec/lc/utils" 11 | "os" 12 | ) 13 | 14 | type Runner struct { 15 | config schema.Options 16 | options *Options 17 | } 18 | 19 | func New(options *Options) (*Runner, error) { 20 | if options.Config == "" { 21 | options.Config = defaultConfigLocation 22 | gologger.Print().Msgf("使用默认配置文件: %s\n", options.Config) 23 | } 24 | checkAndCreateConfigFile(options) 25 | config, err := utils.ReadConfig(options.Config) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return &Runner{config: config, options: options}, nil 30 | } 31 | 32 | func (r *Runner) Enumerate() { 33 | var ( 34 | err error 35 | finalConfig schema.Options 36 | ) 37 | 38 | if r.config, err = utils.ReadConfig(r.options.Config); err != nil { 39 | gologger.Fatal().Msgf("程序配置文件无效,请检查后重试,错误:%s", err) 40 | } 41 | 42 | for _, item := range r.config { 43 | if len(r.options.Provider) != 0 || len(r.options.Id) != 0 { 44 | if len(r.options.Provider) != 0 && !utils.Contains(r.options.Provider, item[utils.Provider]) { 45 | continue 46 | } 47 | if len(r.options.Id) != 0 && !utils.Contains(r.options.Id, item[utils.Id]) { 48 | continue 49 | } 50 | finalConfig = append(finalConfig, item) 51 | } else { 52 | finalConfig = append(finalConfig, item) 53 | } 54 | } 55 | 56 | inventory, err := inventory.New(finalConfig, r.options.CloudServices) 57 | if err != nil { 58 | gologger.Fatal().Msgf("%s", err) 59 | } 60 | var output *os.File 61 | if r.options.Output != "" { 62 | outputFile, err := os.Create(r.options.Output) 63 | if err != nil { 64 | gologger.Fatal().Msgf("无法创建导出的文件 %s: %s\n", r.options.Output, err) 65 | } 66 | output = outputFile 67 | } 68 | builder := &bytes.Buffer{} 69 | schema.SetThreads(r.options.Threads) 70 | for _, provider := range inventory.Providers { 71 | gologger.Info().Msgf("正在列出 %s (%s) 的资产\n", provider.Name(), provider.ID()) 72 | instances, err := provider.Resources(context.Background(), r.options.CloudServices) 73 | if err != nil { 74 | gologger.Error().Msgf("无法获取 %s(%s)的资产: %s\n", provider.Name(), provider.ID(), err) 75 | continue 76 | } 77 | var Count int 78 | for _, instance := range instances.GetItems() { 79 | builder.Reset() 80 | if instance.DNSName != "" { 81 | Count++ 82 | builder.WriteString(instance.DNSName) 83 | builder.WriteRune('\n') 84 | output.WriteString(builder.String()) //nolint 85 | builder.Reset() 86 | gologger.Silent().Msgf("%s", instance.DNSName) 87 | } 88 | if instance.PublicIPv4 != "" { 89 | Count++ 90 | builder.WriteString(instance.PublicIPv4) 91 | builder.WriteRune('\n') 92 | output.WriteString(builder.String()) 93 | builder.Reset() 94 | gologger.Silent().Msgf("%s", instance.PublicIPv4) 95 | } 96 | if instance.PrivateIpv4 != "" && !r.options.ExcludePrivate { 97 | Count++ 98 | builder.WriteString(instance.PrivateIpv4) 99 | builder.WriteRune('\n') 100 | output.WriteString(builder.String()) 101 | builder.Reset() 102 | gologger.Silent().Msgf("%s", instance.PrivateIpv4) 103 | } 104 | } 105 | if Count == 0 { 106 | gologger.Info().Msgf("在 %s (%s) 下未发现资产,这可能是由于权限不足或没有资产,您可以在确认有相关权限后再进行尝试。", provider.Name(), provider.ID()) 107 | } 108 | if !r.options.Silent { 109 | fmt.Println() 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /cmd/options.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/projectdiscovery/gologger/levels" 5 | fileutil "github.com/projectdiscovery/utils/file" 6 | "os" 7 | "os/user" 8 | "path/filepath" 9 | 10 | "github.com/projectdiscovery/goflags" 11 | "github.com/projectdiscovery/gologger" 12 | ) 13 | 14 | type Options struct { 15 | Threads int // Threads 设置线程数量 16 | Silent bool // Silent 只展示结果 17 | Debug bool // Debug 显示详细的输出信息 18 | Version bool // Version 返回工具版本 19 | ExcludePrivate bool // ExcludePrivate 从结果中排除私有 IP 20 | Config string // Config 指定配置文件路径 21 | Output string // Output 将结果写入到文件中 22 | Provider goflags.StringSlice // Provider 指定要列出的云服务商 23 | Id goflags.StringSlice // Id 指定要列出的对象 24 | CloudServices goflags.StringSlice // CloudServices 指定要列出的服务 25 | } 26 | 27 | var ( 28 | defaultConfigLocation = filepath.Join(userHomeDir(), ".config/lc/config.yaml") 29 | ) 30 | 31 | func ParseOptions() *Options { 32 | options := &Options{} 33 | flagSet := goflags.NewFlagSet() 34 | flagSet.SetDescription(`lc (list cloud) 是一个多云攻击面资产梳理工具`) 35 | 36 | flagSet.CreateGroup("config", "配置", 37 | flagSet.StringVarP(&options.Config, "config", "c", defaultConfigLocation, "指定配置文件路径"), 38 | flagSet.IntVarP(&options.Threads, "threads", "t", 3, "指定扫描的线程数量"), 39 | ) 40 | flagSet.CreateGroup("filter", "过滤", 41 | flagSet.StringSliceVarP(&options.CloudServices, "cloud-services", "cs", goflags.StringSlice{"all"}, "指定要列出的服务", 42 | goflags.NormalizedStringSliceOptions), 43 | flagSet.StringSliceVarP(&options.Id, "id", "i", nil, "指定要使用的配置(以逗号分隔)", goflags.NormalizedStringSliceOptions), 44 | flagSet.StringSliceVarP(&options.Provider, "provider", "p", nil, "指定要使用的云服务商(以逗号分隔)", goflags.NormalizedStringSliceOptions), 45 | flagSet.BoolVarP(&options.ExcludePrivate, "exclude-private", "ep", false, "从输出的结果中排除私有 IP"), 46 | ) 47 | flagSet.CreateGroup("output", "输出", 48 | flagSet.StringVarP(&options.Output, "output", "o", "", "将结果输出到指定的文件中"), 49 | flagSet.BoolVarP(&options.Silent, "silent", "s", false, "只输出结果"), 50 | flagSet.BoolVarP(&options.Version, "version", "v", false, "输出工具的版本"), 51 | flagSet.BoolVar(&options.Debug, "debug", false, "输出调试日志信息"), 52 | ) 53 | _ = flagSet.Parse() 54 | options.configureOutput() 55 | showBanner() 56 | if options.Version { 57 | gologger.Info().Msgf("当前版本:%s, 发布日期:%s", version, versionDate) 58 | os.Exit(0) 59 | } 60 | checkAndCreateConfigFile(options) 61 | return options 62 | } 63 | 64 | func (options *Options) configureOutput() { 65 | if options.Silent { 66 | gologger.DefaultLogger.SetMaxLevel(levels.LevelSilent) 67 | } 68 | if options.Debug { 69 | gologger.DefaultLogger.SetMaxLevel(levels.LevelDebug) 70 | } 71 | } 72 | 73 | func userHomeDir() string { 74 | usr, err := user.Current() 75 | if err != nil { 76 | gologger.Fatal().Msgf("Could not get user home directory: %s\n", err) 77 | } 78 | return usr.HomeDir 79 | } 80 | 81 | func checkAndCreateConfigFile(options *Options) { 82 | if options.Config == "" || !fileutil.FileExists(defaultConfigLocation) { 83 | err := os.MkdirAll(filepath.Dir(options.Config), os.ModePerm) 84 | if err != nil { 85 | gologger.Warning().Msgf("无法创建配置文件:%s\n", err) 86 | } 87 | if !fileutil.FileExists(defaultConfigLocation) { 88 | if writeErr := os.WriteFile(defaultConfigLocation, []byte(defaultConfigFile), os.ModePerm); writeErr != nil { 89 | gologger.Warning().Msgf("Could not write default output to %s: %s\n", defaultConfigLocation, writeErr) 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /pkg/schema/validate/validate.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "net" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | var ipv4PrivateRanges = []string{ 10 | "0.0.0.0/8", // 当前网络(仅作为源地址有效) 11 | "10.0.0.0/8", // 私有网络 12 | "100.64.0.0/10", // 共享地址空间 13 | "127.0.0.0/8", // 回路地址 14 | "169.254.0.0/16", // 本地链接(也是许多云提供商的元数据端点) 15 | "172.16.0.0/12", // 私有网络 16 | "192.0.0.0/24", // IETF 协议分配 17 | "192.0.2.0/24", // TEST-NET-1,文档和示例 18 | "192.88.99.0/24", // IPv6 到 IPv4 中继(包括2002::/16) 19 | "192.168.0.0/16", // 私有网络 20 | "198.18.0.0/15", // 网络基准测试 21 | "198.51.100.0/24", // TEST-NET-2,文档和示例 22 | "203.0.113.0/24", // TEST-NET-3,文档和示例 23 | "224.0.0.0/4", // IP组播(以前的 D 类网络) 24 | "240.0.0.0/4", // 预留 (原 E 类网络) 25 | } 26 | 27 | var ipv6PrivateRanges = []string{ 28 | "::1/128", // 回路地址 29 | "64:ff9b::/96", // IPv4/IPv6 转换(RFC 6052) 30 | "100::/64", // 丢弃前缀(RFC 6666) 31 | "2001::/32", // Teredo 隧道 32 | "2001:10::/28", // 已弃用(先前为 ORCHID) 33 | "2001:20::/28", // ORCHIDv2 34 | "2001:db8::/32", // 文档和示例源代码中使用的地址 35 | "2002::/16", // 6to4 36 | "fc00::/7", // 唯一本地地址 37 | "fe80::/10", // 链接地址 38 | "ff00::/8", // 多播 39 | } 40 | 41 | type Validator struct { 42 | ipv4 []*net.IPNet 43 | ipv6 []*net.IPNet 44 | rxDNSName *regexp.Regexp 45 | } 46 | 47 | func NewValidator() (*Validator, error) { 48 | validator := &Validator{} 49 | // regex from: https://github.com/asaskevich/govalidator 50 | validator.rxDNSName = regexp.MustCompile(`^([a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62}){1}(\.[a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62})*[\._]?$`) 51 | 52 | err := validator.appendIPv4Ranges(ipv4PrivateRanges) 53 | if err != nil { 54 | return nil, err 55 | } 56 | err = validator.appendIPv6Ranges(ipv6PrivateRanges) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return validator, nil 61 | } 62 | 63 | type ResourceType int 64 | 65 | const ( 66 | DNSName ResourceType = iota + 1 67 | PublicIP 68 | PrivateIP 69 | None 70 | ) 71 | 72 | func (v *Validator) Identify(item string) ResourceType { 73 | host, port, err := net.SplitHostPort(item) 74 | if err == nil && port != "" { 75 | item = host 76 | } 77 | parsed := net.ParseIP(item) 78 | if v.isDNSName(item, parsed) { 79 | return DNSName 80 | } 81 | if parsed == nil { 82 | return None 83 | } 84 | if strings.Contains(item, ":") { 85 | // 检查 ipv6 私网地址列表 86 | if v.containsIPv6(parsed) { 87 | return PrivateIP 88 | } 89 | return PublicIP 90 | } 91 | // 检查 ipv4 私网地址列表 92 | if v.containsIPv4(parsed) { 93 | return PrivateIP 94 | } 95 | return PublicIP 96 | } 97 | 98 | func (v *Validator) isDNSName(str string, parsed net.IP) bool { 99 | if str == "" || len(strings.Replace(str, ".", "", -1)) > 255 { 100 | return false 101 | } 102 | return !isIP(parsed) && v.rxDNSName.MatchString(str) 103 | } 104 | 105 | func isIP(parsed net.IP) bool { 106 | return parsed != nil 107 | } 108 | 109 | func (v *Validator) appendIPv4Ranges(ranges []string) error { 110 | for _, ip := range ranges { 111 | _, rangeNet, err := net.ParseCIDR(ip) 112 | if err != nil { 113 | return err 114 | } 115 | v.ipv4 = append(v.ipv4, rangeNet) 116 | } 117 | return nil 118 | } 119 | 120 | func (v *Validator) appendIPv6Ranges(ranges []string) error { 121 | for _, ip := range ranges { 122 | _, rangeNet, err := net.ParseCIDR(ip) 123 | if err != nil { 124 | return err 125 | } 126 | v.ipv6 = append(v.ipv6, rangeNet) 127 | } 128 | return nil 129 | } 130 | 131 | func (v *Validator) containsIPv4(IP net.IP) bool { 132 | for _, net := range v.ipv4 { 133 | if net.Contains(IP) { 134 | return true 135 | } 136 | } 137 | return false 138 | } 139 | 140 | func (v *Validator) containsIPv6(IP net.IP) bool { 141 | for _, net := range v.ipv6 { 142 | if net.Contains(IP) { 143 | return true 144 | } 145 | } 146 | return false 147 | } 148 | -------------------------------------------------------------------------------- /pkg/providers/baidu/baidu.go: -------------------------------------------------------------------------------- 1 | package baidu 2 | 3 | import ( 4 | "context" 5 | "github.com/baidubce/bce-sdk-go/auth" 6 | "github.com/baidubce/bce-sdk-go/services/bos" 7 | "github.com/projectdiscovery/goflags" 8 | "github.com/projectdiscovery/gologger" 9 | "github.com/wgpsec/lc/pkg/schema" 10 | "github.com/wgpsec/lc/utils" 11 | "strings" 12 | ) 13 | 14 | type Provider struct { 15 | id string 16 | provider string 17 | bosClient *bos.Client 18 | config providerConfig 19 | cloudServices []string 20 | } 21 | 22 | type providerConfig struct { 23 | accessKeyID string 24 | accessKeySecret string 25 | sessionToken string 26 | okST bool 27 | } 28 | 29 | func New(options schema.OptionBlock, cs goflags.StringSlice) (*Provider, error) { 30 | var ( 31 | endpoint = "https://bj.bcebos.com" 32 | err error 33 | bosClient *bos.Client 34 | cloudServices []string 35 | ) 36 | accessKeyID, ok := options.GetMetadata(utils.AccessKey) 37 | if !ok { 38 | return nil, &utils.ErrNoSuchKey{Name: utils.AccessKey} 39 | } 40 | accessKeySecret, ok := options.GetMetadata(utils.SecretKey) 41 | if !ok { 42 | return nil, &utils.ErrNoSuchKey{Name: utils.SecretKey} 43 | } 44 | id, _ := options.GetMetadata(utils.Id) 45 | sessionToken, okST := options.GetMetadata(utils.SessionToken) 46 | 47 | if okST { 48 | gologger.Debug().Msg("找到百度云访问临时访问凭证") 49 | } else { 50 | gologger.Debug().Msg("找到百度云访问永久访问凭证") 51 | } 52 | 53 | if cs[0] == "all" { 54 | cloudServicesResult, _ := options.GetMetadata(utils.CloudServices) 55 | cloudServices = strings.Split(cloudServicesResult, ",") 56 | } else { 57 | cloudServices = cs 58 | } 59 | for _, cloudService := range cloudServices { 60 | switch cloudService { 61 | case "bos": 62 | // bos client 63 | if okST { 64 | bosClient, err = bos.NewClient(accessKeyID, accessKeySecret, "") 65 | if err != nil { 66 | return nil, err 67 | } 68 | stsCredential, err := auth.NewSessionBceCredentials( 69 | accessKeyID, 70 | accessKeySecret, 71 | sessionToken) 72 | if err != nil { 73 | return nil, err 74 | } 75 | bosClient.Config.Credentials = stsCredential 76 | } else { 77 | clientConfig := bos.BosClientConfiguration{ 78 | Ak: accessKeyID, 79 | Sk: accessKeySecret, 80 | Endpoint: endpoint, 81 | RedirectDisabled: false, 82 | } 83 | bosClient, err = bos.NewClientWithConfig(&clientConfig) 84 | } 85 | if err != nil { 86 | return nil, err 87 | } 88 | } 89 | } 90 | config := providerConfig{ 91 | accessKeyID: accessKeyID, 92 | accessKeySecret: accessKeySecret, 93 | sessionToken: sessionToken, 94 | okST: okST, 95 | } 96 | 97 | return &Provider{provider: utils.Baidu, id: id, bosClient: bosClient, config: config, cloudServices: cloudServices}, nil 98 | } 99 | 100 | func (p *Provider) Resources(ctx context.Context, cs goflags.StringSlice) (*schema.Resources, error) { 101 | finalList := schema.NewResources() 102 | for _, cloudService := range p.cloudServices { 103 | switch cloudService { 104 | case "bcc": 105 | bccProvider := &instanceProvider{provider: p.provider, id: p.id, config: p.config} 106 | lists, err := bccProvider.GetResource(ctx) 107 | if err != nil { 108 | return nil, err 109 | } 110 | gologger.Info().Msgf("获取到 %d 条百度云 BCC 信息", len(lists.GetItems())) 111 | finalList.Merge(lists) 112 | case "bos": 113 | bosProvider := &bosProvider{bosClient: p.bosClient, id: p.id, provider: p.provider} 114 | buckets, err := bosProvider.GetResource(ctx) 115 | if err != nil { 116 | return nil, err 117 | } 118 | gologger.Info().Msgf("获取到 %d 条百度云 BOS 信息", len(buckets.GetItems())) 119 | finalList.Merge(buckets) 120 | } 121 | } 122 | return finalList, nil 123 | } 124 | 125 | func (p *Provider) Name() string { 126 | return p.provider 127 | } 128 | func (p *Provider) ID() string { 129 | return p.id 130 | } 131 | -------------------------------------------------------------------------------- /pkg/providers/aliyun/ecs.go: -------------------------------------------------------------------------------- 1 | package aliyun 2 | 3 | import ( 4 | "context" 5 | "github.com/aliyun/alibaba-cloud-sdk-go/sdk" 6 | "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials" 7 | "github.com/aliyun/alibaba-cloud-sdk-go/services/ecs" 8 | "github.com/projectdiscovery/gologger" 9 | "github.com/wgpsec/lc/pkg/schema" 10 | "sync" 11 | ) 12 | 13 | type instanceProvider struct { 14 | id string 15 | provider string 16 | config providerConfig 17 | ecsRegions *ecs.DescribeRegionsResponse 18 | } 19 | 20 | var ecsList = schema.NewResources() 21 | 22 | func (d *instanceProvider) GetEcsResource(ctx context.Context) (*schema.Resources, error) { 23 | var ( 24 | threads int 25 | err error 26 | wg sync.WaitGroup 27 | regions []string 28 | ) 29 | threads = schema.GetThreads() 30 | 31 | for _, region := range d.ecsRegions.Regions.Region { 32 | regions = append(regions, region.RegionId) 33 | } 34 | 35 | taskCh := make(chan string, threads) 36 | for i := 0; i < threads; i++ { 37 | wg.Add(1) 38 | go func() { 39 | err = d.describeEcsInstances(taskCh, &wg) 40 | if err != nil { 41 | return 42 | } 43 | }() 44 | } 45 | for _, item := range regions { 46 | taskCh <- item 47 | } 48 | close(taskCh) 49 | wg.Wait() 50 | return ecsList, nil 51 | } 52 | 53 | func (d *instanceProvider) describeEcsInstances(ch <-chan string, wg *sync.WaitGroup) error { 54 | defer wg.Done() 55 | var ( 56 | err error 57 | ecsClient *ecs.Client 58 | response *ecs.DescribeInstancesResponse 59 | ) 60 | for region := range ch { 61 | ecsConfig := sdk.NewConfig() 62 | if d.config.okST { 63 | credential := credentials.NewStsTokenCredential(d.config.accessKeyID, d.config.accessKeySecret, d.config.sessionToken) 64 | ecsClient, err = ecs.NewClientWithOptions(region, ecsConfig, credential) 65 | if err != nil { 66 | continue 67 | } 68 | } else { 69 | credential := credentials.NewAccessKeyCredential(d.config.accessKeyID, d.config.accessKeySecret) 70 | ecsClient, err = ecs.NewClientWithOptions(region, ecsConfig, credential) 71 | if err != nil { 72 | continue 73 | } 74 | } 75 | gologger.Debug().Msgf("正在获取 %s 区域下的阿里云 ECS 资源信息", region) 76 | request := ecs.CreateDescribeInstancesRequest() 77 | for { 78 | response, err = ecsClient.DescribeInstances(request) 79 | if err != nil { 80 | break 81 | } 82 | if len(response.Instances.Instance) > 0 { 83 | gologger.Warning().Msgf("在 %s 区域下获取到 %d 条 ECS 资源", region, len(response.Instances.Instance)) 84 | } 85 | for _, instance := range response.Instances.Instance { 86 | var ( 87 | ipv4 []string 88 | privateIPv4 string 89 | ) 90 | if len(instance.PublicIpAddress.IpAddress) > 0 { 91 | ipv4 = append(ipv4, instance.PublicIpAddress.IpAddress...) 92 | } 93 | if len(instance.EipAddress.IpAddress) > 0 { 94 | ipv4 = append(ipv4, instance.EipAddress.IpAddress) 95 | } 96 | if len(instance.NetworkInterfaces.NetworkInterface[0].PrivateIpSets.PrivateIpSet) > 0 { 97 | privateIPv4 = instance.NetworkInterfaces.NetworkInterface[0].PrivateIpSets.PrivateIpSet[0].PrivateIpAddress 98 | } 99 | if len(ipv4) > 0 { 100 | for _, v := range ipv4 { 101 | ecsList.Append(&schema.Resource{ 102 | ID: d.id, 103 | Provider: d.provider, 104 | PublicIPv4: v, 105 | PrivateIpv4: privateIPv4, 106 | Public: true, 107 | }) 108 | } 109 | } else { 110 | ecsList.Append(&schema.Resource{ 111 | ID: d.id, 112 | Provider: d.provider, 113 | PublicIPv4: "", 114 | PrivateIpv4: privateIPv4, 115 | Public: false, 116 | }) 117 | } 118 | 119 | } 120 | if response.NextToken == "" { 121 | gologger.Debug().Msgf("NextToken 为空,已终止获取") 122 | break 123 | } 124 | gologger.Debug().Msgf("NextToken 不为空,正在获取下一页数据") 125 | request.NextToken = response.NextToken 126 | } 127 | } 128 | return err 129 | } 130 | -------------------------------------------------------------------------------- /pkg/schema/schema.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/projectdiscovery/goflags" 7 | "github.com/wgpsec/lc/pkg/schema/validate" 8 | "os" 9 | "strings" 10 | "sync" 11 | ) 12 | 13 | var uniqueMap *sync.Map 14 | var validator *validate.Validator 15 | var Threads int 16 | 17 | type Resources struct { 18 | items []*Resource 19 | sync.RWMutex 20 | } 21 | 22 | func (r *Resources) AppendItem(item *Resource) { 23 | r.Lock() 24 | defer r.Unlock() 25 | r.items = append(r.items, item) 26 | } 27 | func (r *Resources) GetItems() []*Resource { 28 | r.RLock() 29 | defer r.RUnlock() 30 | return r.items 31 | } 32 | 33 | type Provider interface { 34 | Name() string 35 | ID() string 36 | Resources(ctx context.Context, cs goflags.StringSlice) (*Resources, error) 37 | } 38 | 39 | type Resource struct { 40 | Public bool `json:"public"` 41 | Provider string `json:"provider"` 42 | ID string `json:"id,omitempty"` 43 | PublicIPv4 string `json:"public_ipv4,omitempty"` 44 | PrivateIpv4 string `json:"private_ipv4,omitempty"` 45 | DNSName string `json:"dns_name,omitempty"` 46 | } 47 | 48 | type Options []OptionBlock 49 | type OptionBlock map[string]string 50 | 51 | func init() { 52 | uniqueMap = &sync.Map{} 53 | var err error 54 | validator, err = validate.NewValidator() 55 | if err != nil { 56 | panic(fmt.Sprintf("无法创建验证器: %s\n", err)) 57 | } 58 | } 59 | 60 | // Resources 61 | func (r *Resources) appendResource(resource *Resource, uniqueMap *sync.Map) { 62 | if _, ok := uniqueMap.Load(resource.DNSName); !ok && resource.DNSName != "" { 63 | resourceType := validator.Identify(resource.DNSName) 64 | r.appendResourceWithTypeAndMeta(resourceType, resource.DNSName, resource.ID, resource.Provider) 65 | uniqueMap.Store(resource.DNSName, struct{}{}) 66 | } 67 | if _, ok := uniqueMap.Load(resource.PublicIPv4); !ok && resource.PublicIPv4 != "" { 68 | resourceType := validator.Identify(resource.PublicIPv4) 69 | r.appendResourceWithTypeAndMeta(resourceType, resource.PublicIPv4, resource.ID, resource.Provider) 70 | uniqueMap.Store(resource.PublicIPv4, struct{}{}) 71 | } 72 | if _, ok := uniqueMap.Load(resource.PrivateIpv4); !ok && resource.PrivateIpv4 != "" { 73 | resourceType := validator.Identify(resource.PrivateIpv4) 74 | r.appendResourceWithTypeAndMeta(resourceType, resource.PrivateIpv4, resource.ID, resource.Provider) 75 | uniqueMap.Store(resource.PrivateIpv4, struct{}{}) 76 | } 77 | } 78 | 79 | func (r *Resources) appendResourceWithTypeAndMeta(resourceType validate.ResourceType, item, id, provider string) { 80 | resource := &Resource{ 81 | Provider: provider, 82 | ID: id, 83 | } 84 | switch resourceType { 85 | case validate.DNSName: 86 | resource.Public = true 87 | resource.DNSName = item 88 | case validate.PublicIP: 89 | resource.Public = true 90 | resource.PublicIPv4 = item 91 | case validate.PrivateIP: 92 | resource.PrivateIpv4 = item 93 | default: 94 | return 95 | } 96 | r.AppendItem(resource) 97 | } 98 | 99 | func (r *Resources) Append(resource *Resource) { 100 | r.appendResource(resource, uniqueMap) 101 | } 102 | 103 | func (r *Resources) Merge(resources *Resources) { 104 | if resources == nil { 105 | return 106 | } 107 | mergeUniqueMap := &sync.Map{} 108 | for _, item := range resources.GetItems() { 109 | r.appendResource(item, mergeUniqueMap) 110 | } 111 | } 112 | 113 | // OptionBlock 114 | 115 | func (o OptionBlock) GetMetadata(key string) (string, bool) { 116 | data, ok := o[key] 117 | if !ok || data == "" { 118 | return "", false 119 | } 120 | if data[0] == '$' { 121 | envData := os.Getenv(data[1:]) 122 | if envData != "" { 123 | return strings.TrimSpace(envData), true 124 | } 125 | } 126 | return strings.TrimSpace(data), true 127 | } 128 | 129 | // Other 130 | 131 | func NewResources() *Resources { 132 | return &Resources{items: make([]*Resource, 0)} 133 | } 134 | 135 | func SetThreads(threads int) { 136 | Threads = threads 137 | } 138 | 139 | func GetThreads() int { 140 | return Threads 141 | } 142 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wgpsec/lc 2 | 3 | go 1.22.1 4 | 5 | require ( 6 | github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.9 7 | github.com/alibabacloud-go/domain-20180129/v4 v4.2.0 8 | github.com/alibabacloud-go/fc-20230330/v4 v4.1.3 9 | github.com/alibabacloud-go/fc-open-20210406/v2 v2.0.12 10 | github.com/alibabacloud-go/tea-utils/v2 v2.0.6 11 | github.com/aliyun/alibaba-cloud-sdk-go v1.62.712 12 | github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible 13 | github.com/aws/aws-sdk-go v1.51.16 14 | github.com/baidubce/bce-sdk-go v0.9.173 15 | github.com/huaweicloud/huaweicloud-sdk-go-obs v3.23.12+incompatible 16 | github.com/projectdiscovery/goflags v0.1.46 17 | github.com/projectdiscovery/gologger v1.1.12 18 | github.com/projectdiscovery/utils v0.0.87 19 | github.com/qiniu/go-sdk/v7 v7.20.0 20 | github.com/teamssix/oos-go-sdk v0.0.1 21 | github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.893 22 | github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm v1.0.893 23 | github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/lighthouse v1.0.893 24 | github.com/tencentyun/cos-go-sdk-v5 v0.7.47 25 | gopkg.in/yaml.v3 v3.0.1 26 | ) 27 | 28 | require ( 29 | github.com/alibabacloud-go/alibabacloud-gateway-fc-util v0.0.7 // indirect 30 | github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect 31 | github.com/alibabacloud-go/debug v1.0.0 // indirect 32 | github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect 33 | github.com/alibabacloud-go/openapi-util v0.1.0 // indirect 34 | github.com/alibabacloud-go/tea v1.2.2 // indirect 35 | github.com/alibabacloud-go/tea-utils v1.3.1 // indirect 36 | github.com/alibabacloud-go/tea-xml v1.1.3 // indirect 37 | github.com/aliyun/credentials-go v1.3.1 // indirect 38 | github.com/andybalholm/brotli v1.0.6 // indirect 39 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 40 | github.com/aymerick/douceur v0.2.0 // indirect 41 | github.com/clbanning/mxj v1.8.4 // indirect 42 | github.com/clbanning/mxj/v2 v2.5.5 // indirect 43 | github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // indirect 44 | github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect 45 | github.com/gofrs/flock v0.8.1 // indirect 46 | github.com/golang/snappy v0.0.4 // indirect 47 | github.com/google/go-querystring v1.1.0 // indirect 48 | github.com/gorilla/css v1.0.0 // indirect 49 | github.com/jmespath/go-jmespath v0.4.0 // indirect 50 | github.com/json-iterator/go v1.1.12 // indirect 51 | github.com/klauspost/compress v1.16.7 // indirect 52 | github.com/klauspost/pgzip v1.2.5 // indirect 53 | github.com/logrusorgru/aurora v2.0.3+incompatible // indirect 54 | github.com/mholt/archiver/v3 v3.5.1 // indirect 55 | github.com/microcosm-cc/bluemonday v1.0.25 // indirect 56 | github.com/miekg/dns v1.1.56 // indirect 57 | github.com/mitchellh/mapstructure v1.4.3 // indirect 58 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 59 | github.com/modern-go/reflect2 v1.0.2 // indirect 60 | github.com/mozillazg/go-httpheader v0.2.1 // indirect 61 | github.com/nwaples/rardecode v1.1.3 // indirect 62 | github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect 63 | github.com/pierrec/lz4/v4 v4.1.2 // indirect 64 | github.com/pkg/errors v0.9.1 // indirect 65 | github.com/projectdiscovery/blackrock v0.0.1 // indirect 66 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect 67 | github.com/tidwall/gjson v1.14.3 // indirect 68 | github.com/tidwall/match v1.1.1 // indirect 69 | github.com/tidwall/pretty v1.2.0 // indirect 70 | github.com/tjfoc/gmsm v1.3.2 // indirect 71 | github.com/ulikunitz/xz v0.5.11 // indirect 72 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect 73 | golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect 74 | golang.org/x/mod v0.12.0 // indirect 75 | golang.org/x/net v0.20.0 // indirect 76 | golang.org/x/sync v0.6.0 // indirect 77 | golang.org/x/sys v0.17.0 // indirect 78 | golang.org/x/text v0.14.0 // indirect 79 | golang.org/x/time v0.5.0 // indirect 80 | golang.org/x/tools v0.13.0 // indirect 81 | gopkg.in/djherbis/times.v1 v1.3.0 // indirect 82 | gopkg.in/ini.v1 v1.67.0 // indirect 83 | ) 84 | -------------------------------------------------------------------------------- /pkg/providers/aliyun/rds.go: -------------------------------------------------------------------------------- 1 | package aliyun 2 | 3 | import ( 4 | "context" 5 | "github.com/aliyun/alibaba-cloud-sdk-go/sdk" 6 | "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials" 7 | "github.com/aliyun/alibaba-cloud-sdk-go/services/rds" 8 | "github.com/projectdiscovery/gologger" 9 | "github.com/wgpsec/lc/pkg/schema" 10 | "github.com/wgpsec/lc/utils" 11 | "sync" 12 | ) 13 | 14 | type dbInstanceProvider struct { 15 | id string 16 | provider string 17 | config providerConfig 18 | rdsRegions *rds.DescribeRegionsResponse 19 | } 20 | 21 | type rdsInstance struct { 22 | dbId string 23 | region string 24 | } 25 | 26 | var rdsInstances []rdsInstance 27 | var rdsList = schema.NewResources() 28 | 29 | func (d *dbInstanceProvider) GetRdsResource(ctx context.Context) (*schema.Resources, error) { 30 | var ( 31 | threads int 32 | err error 33 | wg sync.WaitGroup 34 | regions []string 35 | ) 36 | threads = schema.GetThreads() 37 | 38 | for _, region := range d.rdsRegions.Regions.RDSRegion { 39 | regions = append(regions, region.RegionId) 40 | } 41 | regions = utils.RemoveRepeatedElement(regions) 42 | 43 | taskCh := make(chan string, threads) 44 | for i := 0; i < threads; i++ { 45 | wg.Add(1) 46 | go func() { 47 | err = d.describeRdsInstances(taskCh, &wg) 48 | if err != nil { 49 | return 50 | } 51 | }() 52 | } 53 | for _, item := range regions { 54 | taskCh <- item 55 | } 56 | close(taskCh) 57 | wg.Wait() 58 | err = d.GetRdsConnectionString(ctx) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return rdsList, nil 63 | } 64 | 65 | func (d *dbInstanceProvider) describeRdsInstances(ch <-chan string, wg *sync.WaitGroup) error { 66 | defer wg.Done() 67 | var ( 68 | err error 69 | rdsClient *rds.Client 70 | response *rds.DescribeDBInstancesResponse 71 | ) 72 | for region := range ch { 73 | rdsConfig := sdk.NewConfig() 74 | if d.config.okST { 75 | credential := credentials.NewStsTokenCredential(d.config.accessKeyID, d.config.accessKeySecret, d.config.sessionToken) 76 | rdsClient, err = rds.NewClientWithOptions(region, rdsConfig, credential) 77 | if err != nil { 78 | continue 79 | } 80 | } else { 81 | credential := credentials.NewAccessKeyCredential(d.config.accessKeyID, d.config.accessKeySecret) 82 | rdsClient, err = rds.NewClientWithOptions(region, rdsConfig, credential) 83 | if err != nil { 84 | continue 85 | } 86 | } 87 | gologger.Debug().Msgf("正在获取 %s 区域下的阿里云 RDS 资源信息", region) 88 | request := rds.CreateDescribeDBInstancesRequest() 89 | for { 90 | response, err = rdsClient.DescribeDBInstances(request) 91 | if err != nil { 92 | break 93 | } 94 | if len(response.Items.DBInstance) > 0 { 95 | gologger.Warning().Msgf("在 %s 区域下获取到 %d 条 RDS 资源", region, len(response.Items.DBInstance)) 96 | } 97 | for _, DBInstance := range response.Items.DBInstance { 98 | rdsInstances = append(rdsInstances, rdsInstance{ 99 | dbId: DBInstance.DBInstanceId, 100 | region: region, 101 | }) 102 | } 103 | if response.NextToken == "" { 104 | gologger.Debug().Msgf("NextToken 为空,已终止获取") 105 | break 106 | } 107 | gologger.Debug().Msgf("NextToken 不为空,正在获取下一页数据") 108 | request.NextToken = response.NextToken 109 | } 110 | } 111 | return err 112 | } 113 | 114 | func (d *dbInstanceProvider) GetRdsConnectionString(ctx context.Context) error { 115 | var ( 116 | private string 117 | public string 118 | err error 119 | rdsClient *rds.Client 120 | response *rds.DescribeDBInstanceNetInfoResponse 121 | ) 122 | for _, dbInstance := range rdsInstances { 123 | gologger.Debug().Msgf("正在获取 %s RDS 实例的连接信息", dbInstance.dbId) 124 | rdsConfig := sdk.NewConfig() 125 | if d.config.okST { 126 | credential := credentials.NewStsTokenCredential(d.config.accessKeyID, d.config.accessKeySecret, d.config.sessionToken) 127 | rdsClient, err = rds.NewClientWithOptions(dbInstance.region, rdsConfig, credential) 128 | if err != nil { 129 | continue 130 | } 131 | } else { 132 | credential := credentials.NewAccessKeyCredential(d.config.accessKeyID, d.config.accessKeySecret) 133 | rdsClient, err = rds.NewClientWithOptions(dbInstance.region, rdsConfig, credential) 134 | if err != nil { 135 | continue 136 | } 137 | } 138 | request := rds.CreateDescribeDBInstanceNetInfoRequest() 139 | request.DBInstanceId = dbInstance.dbId 140 | 141 | response, err = rdsClient.DescribeDBInstanceNetInfo(request) 142 | if err != nil { 143 | return nil 144 | } 145 | for _, DBInstanceNetInfo := range response.DBInstanceNetInfos.DBInstanceNetInfo { 146 | if DBInstanceNetInfo.IPType == "Private" { 147 | private = DBInstanceNetInfo.ConnectionString 148 | } else if DBInstanceNetInfo.IPType == "Public" { 149 | public = DBInstanceNetInfo.ConnectionString 150 | } 151 | } 152 | rdsList.Append(&schema.Resource{ 153 | ID: d.id, 154 | Provider: d.provider, 155 | PublicIPv4: public, 156 | PrivateIpv4: private, 157 | Public: public != "", 158 | }) 159 | } 160 | return err 161 | } 162 | -------------------------------------------------------------------------------- /pkg/providers/tencent/tencent.go: -------------------------------------------------------------------------------- 1 | package tencent 2 | 3 | import ( 4 | "context" 5 | "github.com/projectdiscovery/goflags" 6 | "github.com/projectdiscovery/gologger" 7 | "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" 8 | "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" 9 | "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/regions" 10 | cvm "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cvm/v20170312" 11 | lh "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/lighthouse/v20200324" 12 | cos "github.com/tencentyun/cos-go-sdk-v5" 13 | "github.com/wgpsec/lc/pkg/schema" 14 | "github.com/wgpsec/lc/utils" 15 | "net/http" 16 | "strings" 17 | ) 18 | 19 | type Provider struct { 20 | id string 21 | provider string 22 | credential *common.Credential 23 | cosClient *cos.Client 24 | cvmRegions []*cvm.RegionInfo 25 | lhRegions []*lh.RegionInfo 26 | cloudServices []string 27 | } 28 | 29 | func New(options schema.OptionBlock, cs goflags.StringSlice) (*Provider, error) { 30 | var ( 31 | cosClient *cos.Client 32 | cvmRegions []*cvm.RegionInfo 33 | lhRegions []*lh.RegionInfo 34 | credential *common.Credential 35 | cloudServices []string 36 | ) 37 | accessKeyID, ok := options.GetMetadata(utils.AccessKey) 38 | if !ok { 39 | return nil, &utils.ErrNoSuchKey{Name: utils.AccessKey} 40 | } 41 | accessKeySecret, ok := options.GetMetadata(utils.SecretKey) 42 | if !ok { 43 | return nil, &utils.ErrNoSuchKey{Name: utils.SecretKey} 44 | } 45 | id, _ := options.GetMetadata(utils.Id) 46 | sessionToken, okST := options.GetMetadata(utils.SessionToken) 47 | 48 | if okST { 49 | gologger.Debug().Msg("找到腾讯云访问临时访问凭证") 50 | } else { 51 | gologger.Debug().Msg("找到腾讯云访问永久访问凭证") 52 | } 53 | 54 | if okST { 55 | credential = common.NewTokenCredential(accessKeyID, accessKeySecret, sessionToken) 56 | } else { 57 | credential = common.NewCredential(accessKeyID, accessKeySecret) 58 | } 59 | 60 | if cs[0] == "all" { 61 | cloudServicesResult, _ := options.GetMetadata(utils.CloudServices) 62 | cloudServices = strings.Split(cloudServicesResult, ",") 63 | } else { 64 | cloudServices = cs 65 | } 66 | for _, cloudService := range cloudServices { 67 | switch cloudService { 68 | case "cvm": 69 | // cvm regions 70 | cvmCpf := profile.NewClientProfile() 71 | cvmCpf.HttpProfile.Endpoint = "cvm.tencentcloudapi.com" 72 | cvmClient, err := cvm.NewClient(credential, regions.Beijing, cvmCpf) 73 | cvmRequest := cvm.NewDescribeRegionsRequest() 74 | cvmRequest.SetScheme("https") 75 | cvmResponse, err := cvmClient.DescribeRegions(cvmRequest) 76 | if err != nil { 77 | return nil, err 78 | } 79 | cvmRegions = cvmResponse.Response.RegionSet 80 | case "lh": 81 | // lh regions 82 | lhCpf := profile.NewClientProfile() 83 | lhCpf.HttpProfile.Endpoint = "lighthouse.tencentcloudapi.com" 84 | lhClient, err := lh.NewClient(credential, regions.Beijing, lhCpf) 85 | lhRequest := lh.NewDescribeRegionsRequest() 86 | lhResponse, err := lhClient.DescribeRegions(lhRequest) 87 | if err != nil { 88 | return nil, err 89 | } 90 | lhRegions = lhResponse.Response.RegionSet 91 | case "cos": 92 | // cos client 93 | cosClient = cos.NewClient(nil, &http.Client{ 94 | Transport: &cos.AuthorizationTransport{ 95 | SecretID: accessKeyID, 96 | SecretKey: accessKeySecret, 97 | }, 98 | }) 99 | } 100 | } 101 | 102 | return &Provider{id: id, provider: utils.Tencent, credential: credential, cvmRegions: cvmRegions, lhRegions: lhRegions, cosClient: cosClient, 103 | cloudServices: cloudServices}, 104 | nil 105 | } 106 | 107 | func (p *Provider) Name() string { 108 | return p.provider 109 | } 110 | func (p *Provider) ID() string { 111 | return p.id 112 | } 113 | 114 | func (p *Provider) Resources(ctx context.Context, cs goflags.StringSlice) (*schema.Resources, error) { 115 | finalList := schema.NewResources() 116 | for _, cloudService := range p.cloudServices { 117 | switch cloudService { 118 | case "cvm": 119 | cvmProvider := &instanceProvider{id: p.id, provider: p.provider, cvmRegions: p.cvmRegions, lhRegions: p.lhRegions, credential: p.credential} 120 | cvmList, err := cvmProvider.GetCVMResource(ctx) 121 | if err != nil { 122 | return nil, err 123 | } 124 | gologger.Info().Msgf("获取到 %d 条腾讯云 CVM 信息", len(cvmList.GetItems())) 125 | finalList.Merge(cvmList) 126 | case "lh": 127 | lhProvider := &instanceProvider{id: p.id, provider: p.provider, cvmRegions: p.cvmRegions, lhRegions: p.lhRegions, credential: p.credential} 128 | lhList, err := lhProvider.GetLHResource(ctx) 129 | if err != nil { 130 | return nil, err 131 | } 132 | gologger.Info().Msgf("获取到 %d 条腾讯云 LH 信息", len(lhList.GetItems())) 133 | finalList.Merge(lhList) 134 | case "cos": 135 | cosProvider := &cosProvider{provider: p.provider, id: p.id, cosClient: p.cosClient} 136 | cosList, err := cosProvider.GetResource(ctx) 137 | if err != nil { 138 | return nil, err 139 | } 140 | gologger.Info().Msgf("获取到 %d 条腾讯云 COS 信息", len(cosList.GetItems())) 141 | finalList.Merge(cosList) 142 | } 143 | } 144 | return finalList, nil 145 | } 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

14 | 功能 • 15 | 安装 • 16 | 配置 • 17 | 使用 • 18 | 支持的云服务商 19 |
20 | 21 | LC(List Cloud)是一个多云攻击面资产梳理的工具,使用 LC 可以让甲方蓝队在管理多云时快速梳理出可能暴露在公网上的资产。 22 | 23 | ## 功能 24 | 25 | - 列出多个配置的云资产 26 | - 支持多个云服务商 27 | - 支持多个云服务 28 | - 支持过滤内网 IP 29 | - 高度可扩展性,可方便添加更多云服务商和云服务 30 | - 可以使用管道符和其他工具结合使用 31 | 32 | 运行截图: 33 | 34 |

151 | ![]() TeamsSix 152 | |
153 |
154 | ![]() ShuBo6 155 | |
156 |
157 | ![]() tari 158 | |
159 |


